@kernel.chat/kbot 1.3.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +94 -0
  2. package/dist/agent.d.ts +9 -0
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +576 -119
  5. package/dist/agent.js.map +1 -1
  6. package/dist/auth.d.ts +20 -35
  7. package/dist/auth.d.ts.map +1 -1
  8. package/dist/auth.js +236 -66
  9. package/dist/auth.js.map +1 -1
  10. package/dist/auth.test.d.ts +2 -0
  11. package/dist/auth.test.d.ts.map +1 -0
  12. package/dist/auth.test.js +89 -0
  13. package/dist/auth.test.js.map +1 -0
  14. package/dist/build-targets.d.ts +37 -0
  15. package/dist/build-targets.d.ts.map +1 -0
  16. package/dist/build-targets.js +507 -0
  17. package/dist/build-targets.js.map +1 -0
  18. package/dist/cli.js +1211 -131
  19. package/dist/cli.js.map +1 -1
  20. package/dist/context.d.ts +2 -0
  21. package/dist/context.d.ts.map +1 -1
  22. package/dist/context.js +72 -22
  23. package/dist/context.js.map +1 -1
  24. package/dist/hooks.d.ts +27 -0
  25. package/dist/hooks.d.ts.map +1 -0
  26. package/dist/hooks.js +145 -0
  27. package/dist/hooks.js.map +1 -0
  28. package/dist/ide/acp-server.d.ts +6 -0
  29. package/dist/ide/acp-server.d.ts.map +1 -0
  30. package/dist/ide/acp-server.js +319 -0
  31. package/dist/ide/acp-server.js.map +1 -0
  32. package/dist/ide/bridge.d.ts +128 -0
  33. package/dist/ide/bridge.d.ts.map +1 -0
  34. package/dist/ide/bridge.js +185 -0
  35. package/dist/ide/bridge.js.map +1 -0
  36. package/dist/ide/index.d.ts +5 -0
  37. package/dist/ide/index.d.ts.map +1 -0
  38. package/dist/ide/index.js +11 -0
  39. package/dist/ide/index.js.map +1 -0
  40. package/dist/ide/lsp-bridge.d.ts +27 -0
  41. package/dist/ide/lsp-bridge.d.ts.map +1 -0
  42. package/dist/ide/lsp-bridge.js +267 -0
  43. package/dist/ide/lsp-bridge.js.map +1 -0
  44. package/dist/ide/mcp-server.d.ts +7 -0
  45. package/dist/ide/mcp-server.d.ts.map +1 -0
  46. package/dist/ide/mcp-server.js +451 -0
  47. package/dist/ide/mcp-server.js.map +1 -0
  48. package/dist/learning.d.ts +179 -0
  49. package/dist/learning.d.ts.map +1 -0
  50. package/dist/learning.js +829 -0
  51. package/dist/learning.js.map +1 -0
  52. package/dist/learning.test.d.ts +2 -0
  53. package/dist/learning.test.d.ts.map +1 -0
  54. package/dist/learning.test.js +115 -0
  55. package/dist/learning.test.js.map +1 -0
  56. package/dist/matrix.d.ts +49 -0
  57. package/dist/matrix.d.ts.map +1 -0
  58. package/dist/matrix.js +302 -0
  59. package/dist/matrix.js.map +1 -0
  60. package/dist/memory.d.ts +11 -0
  61. package/dist/memory.d.ts.map +1 -1
  62. package/dist/memory.js +54 -2
  63. package/dist/memory.js.map +1 -1
  64. package/dist/multimodal.d.ts +57 -0
  65. package/dist/multimodal.d.ts.map +1 -0
  66. package/dist/multimodal.js +206 -0
  67. package/dist/multimodal.js.map +1 -0
  68. package/dist/permissions.d.ts +21 -0
  69. package/dist/permissions.d.ts.map +1 -0
  70. package/dist/permissions.js +122 -0
  71. package/dist/permissions.js.map +1 -0
  72. package/dist/planner.d.ts +54 -0
  73. package/dist/planner.d.ts.map +1 -0
  74. package/dist/planner.js +298 -0
  75. package/dist/planner.js.map +1 -0
  76. package/dist/plugins.d.ts +30 -0
  77. package/dist/plugins.d.ts.map +1 -0
  78. package/dist/plugins.js +135 -0
  79. package/dist/plugins.js.map +1 -0
  80. package/dist/sessions.d.ts +38 -0
  81. package/dist/sessions.d.ts.map +1 -0
  82. package/dist/sessions.js +177 -0
  83. package/dist/sessions.js.map +1 -0
  84. package/dist/streaming.d.ts +88 -0
  85. package/dist/streaming.d.ts.map +1 -0
  86. package/dist/streaming.js +317 -0
  87. package/dist/streaming.js.map +1 -0
  88. package/dist/tools/background.d.ts +2 -0
  89. package/dist/tools/background.d.ts.map +1 -0
  90. package/dist/tools/background.js +163 -0
  91. package/dist/tools/background.js.map +1 -0
  92. package/dist/tools/bash.d.ts.map +1 -1
  93. package/dist/tools/bash.js +26 -1
  94. package/dist/tools/bash.js.map +1 -1
  95. package/dist/tools/browser.js +7 -7
  96. package/dist/tools/browser.js.map +1 -1
  97. package/dist/tools/build-matrix.d.ts +2 -0
  98. package/dist/tools/build-matrix.d.ts.map +1 -0
  99. package/dist/tools/build-matrix.js +463 -0
  100. package/dist/tools/build-matrix.js.map +1 -0
  101. package/dist/tools/computer.js +5 -5
  102. package/dist/tools/computer.js.map +1 -1
  103. package/dist/tools/fetch.d.ts +2 -0
  104. package/dist/tools/fetch.d.ts.map +1 -0
  105. package/dist/tools/fetch.js +106 -0
  106. package/dist/tools/fetch.js.map +1 -0
  107. package/dist/tools/files.d.ts.map +1 -1
  108. package/dist/tools/files.js +112 -6
  109. package/dist/tools/files.js.map +1 -1
  110. package/dist/tools/git.js +3 -3
  111. package/dist/tools/git.js.map +1 -1
  112. package/dist/tools/github.d.ts +2 -0
  113. package/dist/tools/github.d.ts.map +1 -0
  114. package/dist/tools/github.js +196 -0
  115. package/dist/tools/github.js.map +1 -0
  116. package/dist/tools/index.d.ts +29 -5
  117. package/dist/tools/index.d.ts.map +1 -1
  118. package/dist/tools/index.js +136 -20
  119. package/dist/tools/index.js.map +1 -1
  120. package/dist/tools/index.test.d.ts +2 -0
  121. package/dist/tools/index.test.d.ts.map +1 -0
  122. package/dist/tools/index.test.js +162 -0
  123. package/dist/tools/index.test.js.map +1 -0
  124. package/dist/tools/matrix.d.ts +2 -0
  125. package/dist/tools/matrix.d.ts.map +1 -0
  126. package/dist/tools/matrix.js +79 -0
  127. package/dist/tools/matrix.js.map +1 -0
  128. package/dist/tools/mcp-client.d.ts +2 -0
  129. package/dist/tools/mcp-client.d.ts.map +1 -0
  130. package/dist/tools/mcp-client.js +295 -0
  131. package/dist/tools/mcp-client.js.map +1 -0
  132. package/dist/tools/notebook.d.ts +2 -0
  133. package/dist/tools/notebook.d.ts.map +1 -0
  134. package/dist/tools/notebook.js +207 -0
  135. package/dist/tools/notebook.js.map +1 -0
  136. package/dist/tools/openclaw.d.ts +2 -0
  137. package/dist/tools/openclaw.d.ts.map +1 -0
  138. package/dist/tools/openclaw.js +187 -0
  139. package/dist/tools/openclaw.js.map +1 -0
  140. package/dist/tools/parallel.d.ts +2 -0
  141. package/dist/tools/parallel.d.ts.map +1 -0
  142. package/dist/tools/parallel.js +60 -0
  143. package/dist/tools/parallel.js.map +1 -0
  144. package/dist/tools/sandbox.d.ts +2 -0
  145. package/dist/tools/sandbox.d.ts.map +1 -0
  146. package/dist/tools/sandbox.js +352 -0
  147. package/dist/tools/sandbox.js.map +1 -0
  148. package/dist/tools/search.d.ts.map +1 -1
  149. package/dist/tools/search.js +135 -28
  150. package/dist/tools/search.js.map +1 -1
  151. package/dist/tools/subagent.d.ts +4 -0
  152. package/dist/tools/subagent.d.ts.map +1 -0
  153. package/dist/tools/subagent.js +260 -0
  154. package/dist/tools/subagent.js.map +1 -0
  155. package/dist/tools/tasks.d.ts +14 -0
  156. package/dist/tools/tasks.d.ts.map +1 -0
  157. package/dist/tools/tasks.js +210 -0
  158. package/dist/tools/tasks.js.map +1 -0
  159. package/dist/tools/worktree.d.ts +2 -0
  160. package/dist/tools/worktree.d.ts.map +1 -0
  161. package/dist/tools/worktree.js +223 -0
  162. package/dist/tools/worktree.js.map +1 -0
  163. package/dist/tui.d.ts +73 -0
  164. package/dist/tui.d.ts.map +1 -0
  165. package/dist/tui.js +257 -0
  166. package/dist/tui.js.map +1 -0
  167. package/dist/ui.d.ts +11 -19
  168. package/dist/ui.d.ts.map +1 -1
  169. package/dist/ui.js +143 -171
  170. package/dist/ui.js.map +1 -1
  171. package/dist/updater.d.ts +3 -0
  172. package/dist/updater.d.ts.map +1 -0
  173. package/dist/updater.js +70 -0
  174. package/dist/updater.js.map +1 -0
  175. package/install.sh +5 -7
  176. package/package.json +8 -4
@@ -0,0 +1,829 @@
1
+ // K:BOT Recursive Language Learning Engine
2
+ //
3
+ // GOAL: Reduce token and message usage over time by learning from interactions.
4
+ //
5
+ // Three systems:
6
+ // 1. PATTERN CACHE — Cache successful tool sequences so repeat problems skip the API
7
+ // 2. SOLUTION INDEX — Extract reusable solutions from past conversations
8
+ // 3. USER PROFILE — Learn user preferences, style, and common workflows
9
+ //
10
+ // Everything persists to ~/.kbot/memory/ as JSON files.
11
+ import { homedir } from 'node:os';
12
+ import { join } from 'node:path';
13
+ import { existsSync, readFileSync, writeFileSync, writeFile, mkdirSync } from 'node:fs';
14
+ const LEARN_DIR = join(homedir(), '.kbot', 'memory');
15
+ const PATTERNS_FILE = join(LEARN_DIR, 'patterns.json');
16
+ const SOLUTIONS_FILE = join(LEARN_DIR, 'solutions.json');
17
+ const PROFILE_FILE = join(LEARN_DIR, 'profile.json');
18
+ const STATS_FILE = join(LEARN_DIR, 'stats.json');
19
+ const KNOWLEDGE_FILE = join(LEARN_DIR, 'knowledge.json');
20
+ const CORRECTIONS_FILE = join(LEARN_DIR, 'corrections.json');
21
+ const PROJECTS_FILE = join(LEARN_DIR, 'projects.json');
22
+ function ensureDir() {
23
+ if (!existsSync(LEARN_DIR))
24
+ mkdirSync(LEARN_DIR, { recursive: true });
25
+ }
26
+ function loadJSON(path, fallback) {
27
+ ensureDir();
28
+ if (!existsSync(path))
29
+ return fallback;
30
+ try {
31
+ return JSON.parse(readFileSync(path, 'utf-8'));
32
+ }
33
+ catch {
34
+ return fallback;
35
+ }
36
+ }
37
+ /** Debounced async file writer — batches multiple writes into one per file */
38
+ const pendingWrites = new Map();
39
+ const WRITE_DEBOUNCE_MS = 500;
40
+ function saveJSON(path, data) {
41
+ ensureDir();
42
+ // Cancel any pending write for this file
43
+ const existing = pendingWrites.get(path);
44
+ if (existing)
45
+ clearTimeout(existing);
46
+ // Debounce — only write after 500ms of no new saves to this file
47
+ const timer = setTimeout(() => {
48
+ pendingWrites.delete(path);
49
+ writeFile(path, JSON.stringify(data, null, 2), (err) => {
50
+ if (err) { /* non-critical — learning data can be regenerated */ }
51
+ });
52
+ }, WRITE_DEBOUNCE_MS);
53
+ pendingWrites.set(path, timer);
54
+ }
55
+ /** Synchronous save for critical data (config, not learning) */
56
+ function saveJSONSync(path, data) {
57
+ ensureDir();
58
+ writeFileSync(path, JSON.stringify(data, null, 2));
59
+ }
60
+ /** Flush all pending writes immediately (call on exit) */
61
+ export function flushPendingWrites() {
62
+ for (const [path, timer] of pendingWrites.entries()) {
63
+ clearTimeout(timer);
64
+ pendingWrites.delete(path);
65
+ // Read the latest data reference — we need to save what's in memory
66
+ }
67
+ // Save current state of all mutable data
68
+ try {
69
+ writeFileSync(PATTERNS_FILE, JSON.stringify(patterns, null, 2));
70
+ writeFileSync(SOLUTIONS_FILE, JSON.stringify(solutions, null, 2));
71
+ writeFileSync(PROFILE_FILE, JSON.stringify(profile, null, 2));
72
+ writeFileSync(KNOWLEDGE_FILE, JSON.stringify(knowledge, null, 2));
73
+ writeFileSync(CORRECTIONS_FILE, JSON.stringify(corrections, null, 2));
74
+ writeFileSync(PROJECTS_FILE, JSON.stringify(projects, null, 2));
75
+ }
76
+ catch { /* best-effort */ }
77
+ }
78
+ let patterns = loadJSON(PATTERNS_FILE, []);
79
+ /** Normalize a message into an intent signature — preserves word order for context */
80
+ function normalizeIntent(message) {
81
+ const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
82
+ 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should',
83
+ 'may', 'might', 'shall', 'can', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by',
84
+ 'from', 'it', 'this', 'that', 'these', 'those', 'i', 'me', 'my', 'we', 'you', 'your',
85
+ 'he', 'she', 'they', 'them', 'and', 'or', 'but', 'not', 'so', 'if', 'then', 'please']);
86
+ // Preserve word order (don't sort) — order carries intent context
87
+ return message.toLowerCase()
88
+ .replace(/[^a-z0-9\s]/g, ' ')
89
+ .split(/\s+/)
90
+ .filter(w => w.length > 2 && !stopWords.has(w))
91
+ .join(' ');
92
+ }
93
+ /** Extract keywords from a message */
94
+ export function extractKeywords(message) {
95
+ const techTerms = new Set(['react', 'typescript', 'node', 'python', 'rust', 'go', 'docker',
96
+ 'api', 'database', 'test', 'deploy', 'build', 'fix', 'bug', 'error', 'component',
97
+ 'function', 'class', 'import', 'export', 'async', 'await', 'fetch', 'route', 'auth',
98
+ 'css', 'html', 'json', 'sql', 'git', 'npm', 'install', 'config', 'env', 'server',
99
+ 'client', 'hook', 'state', 'redux', 'zustand', 'supabase', 'stripe', 'vite', 'webpack']);
100
+ return message.toLowerCase()
101
+ .replace(/[^a-z0-9\s]/g, ' ')
102
+ .split(/\s+/)
103
+ .filter(w => w.length > 2 && techTerms.has(w));
104
+ }
105
+ /** Find a matching cached pattern (similarity > 0.6) */
106
+ export function findPattern(message) {
107
+ const intent = normalizeIntent(message);
108
+ const keywords = extractKeywords(message);
109
+ if (!intent)
110
+ return null;
111
+ let bestMatch = null;
112
+ let bestScore = 0;
113
+ for (const p of patterns) {
114
+ // Jaccard similarity on intent words
115
+ const intentWords = new Set(intent.split(' '));
116
+ const patternWords = new Set(p.intent.split(' '));
117
+ const intersection = [...intentWords].filter(w => patternWords.has(w)).length;
118
+ const union = new Set([...intentWords, ...patternWords]).size;
119
+ const intentSim = union > 0 ? intersection / union : 0;
120
+ // Keyword overlap bonus
121
+ const kwOverlap = keywords.filter(k => p.keywords.includes(k)).length;
122
+ const kwBonus = keywords.length > 0 ? (kwOverlap / keywords.length) * 0.3 : 0;
123
+ // Frequency boost (well-tested patterns are more reliable)
124
+ const freqBoost = Math.min(p.hits / 10, 0.1);
125
+ const score = intentSim + kwBonus + freqBoost;
126
+ if (score > bestScore && score > 0.6 && p.successRate > 0.5) {
127
+ bestScore = score;
128
+ bestMatch = p;
129
+ }
130
+ }
131
+ return bestMatch;
132
+ }
133
+ /** Record a successful pattern */
134
+ export function recordPattern(message, toolSequence, tokensSaved = 0) {
135
+ if (toolSequence.length === 0)
136
+ return;
137
+ const intent = normalizeIntent(message);
138
+ const keywords = extractKeywords(message);
139
+ if (!intent)
140
+ return;
141
+ const existing = patterns.find(p => p.intent === intent);
142
+ if (existing) {
143
+ existing.hits++;
144
+ existing.successRate = (existing.successRate * (existing.hits - 1) + 1) / existing.hits;
145
+ existing.lastUsed = new Date().toISOString();
146
+ existing.avgTokensSaved = (existing.avgTokensSaved * (existing.hits - 1) + tokensSaved) / existing.hits;
147
+ // Update tool sequence if this one is shorter (more efficient)
148
+ if (toolSequence.length < existing.toolSequence.length) {
149
+ existing.toolSequence = toolSequence;
150
+ }
151
+ }
152
+ else {
153
+ patterns.push({
154
+ intent, keywords, toolSequence,
155
+ hits: 1, successRate: 1.0,
156
+ lastUsed: new Date().toISOString(),
157
+ avgTokensSaved: tokensSaved,
158
+ });
159
+ }
160
+ // Keep top 100 patterns by score (hits * successRate)
161
+ patterns.sort((a, b) => (b.hits * b.successRate) - (a.hits * a.successRate));
162
+ patterns = patterns.slice(0, 100);
163
+ saveJSON(PATTERNS_FILE, patterns);
164
+ }
165
+ /** Record a failed pattern */
166
+ export function recordPatternFailure(message) {
167
+ const intent = normalizeIntent(message);
168
+ const existing = patterns.find(p => p.intent === intent);
169
+ if (existing) {
170
+ existing.successRate = (existing.successRate * existing.hits) / (existing.hits + 1);
171
+ existing.hits++;
172
+ saveJSON(PATTERNS_FILE, patterns);
173
+ }
174
+ }
175
+ let solutions = loadJSON(SOLUTIONS_FILE, []);
176
+ /** Find relevant cached solutions for a message */
177
+ export function findSolutions(message, maxResults = 3) {
178
+ const keywords = extractKeywords(message);
179
+ const messageWords = new Set(message.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 2));
180
+ const scored = solutions.map(s => {
181
+ const kwOverlap = keywords.filter(k => s.keywords.includes(k)).length;
182
+ const kwScore = keywords.length > 0 ? kwOverlap / keywords.length : 0;
183
+ // Word overlap with question
184
+ const qWords = new Set(s.question.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 2));
185
+ const overlap = [...messageWords].filter(w => qWords.has(w)).length;
186
+ const wordScore = qWords.size > 0 ? overlap / qWords.size : 0;
187
+ const score = kwScore * 0.4 + wordScore * 0.4 + s.confidence * 0.2;
188
+ return { solution: s, score };
189
+ });
190
+ const results = scored
191
+ .filter(s => s.score > 0.3)
192
+ .sort((a, b) => b.score - a.score)
193
+ .slice(0, maxResults)
194
+ .map(s => s.solution);
195
+ // Update reuse counts separately (don't mutate during search)
196
+ if (results.length > 0) {
197
+ for (const s of results)
198
+ s.reuses++;
199
+ saveJSON(SOLUTIONS_FILE, solutions);
200
+ }
201
+ return results;
202
+ }
203
+ /** Cache a solution from a successful interaction */
204
+ export function cacheSolution(question, solution) {
205
+ if (solution.length < 20 || solution.length > 5000)
206
+ return;
207
+ const keywords = extractKeywords(question);
208
+ const normalized = normalizeIntent(question);
209
+ // Don't duplicate similar solutions
210
+ const existing = solutions.find(s => normalizeIntent(s.question) === normalized);
211
+ if (existing) {
212
+ existing.confidence = Math.min(1, existing.confidence + 0.1);
213
+ existing.solution = solution; // Update with latest
214
+ saveJSON(SOLUTIONS_FILE, solutions);
215
+ return;
216
+ }
217
+ solutions.push({
218
+ question: question.slice(0, 200),
219
+ keywords,
220
+ solution: solution.slice(0, 3000),
221
+ confidence: 0.7,
222
+ reuses: 0,
223
+ created: new Date().toISOString(),
224
+ });
225
+ // Keep top 200 solutions
226
+ solutions.sort((a, b) => (b.confidence * (b.reuses + 1)) - (a.confidence * (a.reuses + 1)));
227
+ solutions = solutions.slice(0, 200);
228
+ saveJSON(SOLUTIONS_FILE, solutions);
229
+ }
230
+ let profile = loadJSON(PROFILE_FILE, {
231
+ responseStyle: 'auto',
232
+ techStack: [],
233
+ taskPatterns: {},
234
+ preferredAgents: {},
235
+ totalMessages: 0,
236
+ totalTokens: 0,
237
+ tokensSaved: 0,
238
+ avgTokensPerMessage: 0,
239
+ sessions: 0,
240
+ });
241
+ /** Tech stack usage frequency for decay-based ranking */
242
+ let techStackFrequency = loadJSON(join(LEARN_DIR, 'tech-freq.json'), {});
243
+ export function getProfile() {
244
+ return profile;
245
+ }
246
+ /** Update profile after each interaction */
247
+ export function updateProfile(opts) {
248
+ if (opts.tokens) {
249
+ profile.totalMessages++;
250
+ profile.totalTokens += opts.tokens;
251
+ profile.avgTokensPerMessage =
252
+ profile.totalTokens / profile.totalMessages;
253
+ }
254
+ if (opts.tokensSaved) {
255
+ profile.tokensSaved += opts.tokensSaved;
256
+ }
257
+ if (opts.agent && opts.agent !== 'local') {
258
+ profile.preferredAgents[opts.agent] = (profile.preferredAgents[opts.agent] || 0) + 1;
259
+ }
260
+ if (opts.taskType) {
261
+ profile.taskPatterns[opts.taskType] = (profile.taskPatterns[opts.taskType] || 0) + 1;
262
+ }
263
+ if (opts.techTerms && opts.techTerms.length > 0) {
264
+ // Tech stack with frequency tracking for decay
265
+ if (!techStackFrequency)
266
+ techStackFrequency = {};
267
+ for (const t of opts.techTerms) {
268
+ techStackFrequency[t] = (techStackFrequency[t] || 0) + 1;
269
+ }
270
+ // Rebuild techStack from frequency — most used terms first, with decay
271
+ profile.techStack = Object.entries(techStackFrequency)
272
+ .sort((a, b) => b[1] - a[1])
273
+ .slice(0, 20)
274
+ .map(([term]) => term);
275
+ }
276
+ saveJSON(PROFILE_FILE, profile);
277
+ }
278
+ export function incrementSessions() {
279
+ profile.sessions++;
280
+ saveJSON(PROFILE_FILE, profile);
281
+ return profile.sessions;
282
+ }
283
+ // ═══ 4. LEARNING CONTEXT BUILDER ════════════════════════════════
284
+ // Builds the most efficient context for each message by combining
285
+ // cached patterns, relevant solutions, and user profile.
286
+ // This replaces dumping the entire memory file into every prompt.
287
+ export function buildLearningContext(message) {
288
+ const parts = [];
289
+ // A. Pattern hint — if we've solved this type of problem before
290
+ const pattern = findPattern(message);
291
+ if (pattern) {
292
+ parts.push(`[Learned Pattern — ${pattern.hits}x success, ${Math.round(pattern.successRate * 100)}% rate]`, `Similar tasks were solved with: ${pattern.toolSequence.join(' → ')}`, `Hint: follow this tool sequence to solve efficiently in fewer steps.`);
293
+ }
294
+ // B. Relevant solutions — inject only matching ones, not entire history
295
+ const relevant = findSolutions(message, 2);
296
+ if (relevant.length > 0) {
297
+ parts.push('[Cached Solutions]');
298
+ for (const s of relevant) {
299
+ parts.push(`Q: ${s.question}\nA: ${s.solution}\n`);
300
+ }
301
+ }
302
+ // C. User profile hints — help the AI match user expectations
303
+ if (profile.totalMessages > 5) {
304
+ const hints = [];
305
+ if (profile.techStack.length > 0) {
306
+ hints.push(`User's stack: ${profile.techStack.join(', ')}`);
307
+ }
308
+ if (profile.responseStyle !== 'auto') {
309
+ hints.push(`Preferred style: ${profile.responseStyle}`);
310
+ }
311
+ // Most common task type
312
+ const topTask = Object.entries(profile.taskPatterns)
313
+ .sort((a, b) => b[1] - a[1])[0];
314
+ if (topTask && topTask[1] > 3) {
315
+ hints.push(`Most common task: ${topTask[0]}`);
316
+ }
317
+ if (hints.length > 0) {
318
+ parts.push(`[User Profile]\n${hints.join('\n')}`);
319
+ }
320
+ }
321
+ return parts.length > 0 ? parts.join('\n\n') : '';
322
+ }
323
+ export function getStats() {
324
+ // Baseline: assume 2000 tokens per message without learning
325
+ const baseline = 2000;
326
+ const actual = profile.avgTokensPerMessage || baseline;
327
+ const efficiencyPct = actual < baseline
328
+ ? Math.round((1 - actual / baseline) * 100)
329
+ : 0;
330
+ return {
331
+ patternsCount: patterns.length,
332
+ solutionsCount: solutions.length,
333
+ totalTokensSaved: profile.tokensSaved,
334
+ avgTokensPerMsg: Math.round(profile.avgTokensPerMessage),
335
+ totalMessages: profile.totalMessages,
336
+ sessions: profile.sessions,
337
+ efficiency: efficiencyPct > 0 ? `${efficiencyPct}% more efficient` : 'Baseline (learning...)',
338
+ };
339
+ }
340
+ /** Classify task type from message */
341
+ export function classifyTask(message) {
342
+ const lower = message.toLowerCase();
343
+ if (/\b(fix|bug|error|issue|broken|crash|fail)\b/.test(lower))
344
+ return 'debug';
345
+ if (/\b(build|create|scaffold|generate|new|init|setup)\b/.test(lower))
346
+ return 'build';
347
+ if (/\b(refactor|clean|reorganize|restructure|simplify)\b/.test(lower))
348
+ return 'refactor';
349
+ if (/\b(test|spec|coverage|assert)\b/.test(lower))
350
+ return 'test';
351
+ if (/\b(deploy|ship|release|publish)\b/.test(lower))
352
+ return 'deploy';
353
+ if (/\b(explain|what|how|why|describe)\b/.test(lower))
354
+ return 'explain';
355
+ if (/\b(review|audit|check|inspect)\b/.test(lower))
356
+ return 'review';
357
+ if (/\b(search|find|grep|locate|where)\b/.test(lower))
358
+ return 'search';
359
+ return 'general';
360
+ }
361
+ let knowledge = loadJSON(KNOWLEDGE_FILE, []);
362
+ /** Store a knowledge entry (user teaches kbot something) */
363
+ export function learnFact(fact, category = 'fact', source = 'user-taught') {
364
+ if (!fact || fact.length < 5)
365
+ return;
366
+ const keywords = fact.toLowerCase()
367
+ .replace(/[^a-z0-9\s]/g, ' ')
368
+ .split(/\s+/)
369
+ .filter(w => w.length > 2);
370
+ // Check for duplicate or similar (use spread to avoid mutating stored arrays)
371
+ const normalized = [...keywords].sort().join(' ');
372
+ const existing = knowledge.find(k => {
373
+ const kNorm = [...k.keywords].sort().join(' ');
374
+ return kNorm === normalized;
375
+ });
376
+ if (existing) {
377
+ existing.fact = fact; // Update with latest wording
378
+ existing.confidence = Math.min(1, existing.confidence + 0.1);
379
+ existing.lastUsed = new Date().toISOString();
380
+ saveJSON(KNOWLEDGE_FILE, knowledge);
381
+ return;
382
+ }
383
+ knowledge.push({
384
+ fact,
385
+ category,
386
+ keywords,
387
+ source,
388
+ confidence: source === 'user-taught' ? 1.0 : 0.7,
389
+ references: 0,
390
+ created: new Date().toISOString(),
391
+ lastUsed: new Date().toISOString(),
392
+ });
393
+ // Keep top 500 knowledge entries
394
+ knowledge.sort((a, b) => (b.confidence * (b.references + 1)) - (a.confidence * (a.references + 1)));
395
+ knowledge = knowledge.slice(0, 500);
396
+ saveJSON(KNOWLEDGE_FILE, knowledge);
397
+ }
398
+ /** Find relevant knowledge for a message */
399
+ export function findKnowledge(message, maxResults = 5) {
400
+ const msgWords = new Set(message.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 2));
401
+ const scored = knowledge.map(k => {
402
+ const overlap = k.keywords.filter(kw => msgWords.has(kw)).length;
403
+ const score = k.keywords.length > 0
404
+ ? (overlap / k.keywords.length) * 0.6 + k.confidence * 0.3 + Math.min(k.references / 10, 0.1)
405
+ : 0;
406
+ return { entry: k, score };
407
+ });
408
+ const results = scored
409
+ .filter(s => s.score > 0.2)
410
+ .sort((a, b) => b.score - a.score)
411
+ .slice(0, maxResults)
412
+ .map(s => s.entry);
413
+ // Update reference counts separately (don't mutate during search)
414
+ if (results.length > 0) {
415
+ const now = new Date().toISOString();
416
+ for (const entry of results) {
417
+ entry.references++;
418
+ entry.lastUsed = now;
419
+ }
420
+ saveJSON(KNOWLEDGE_FILE, knowledge);
421
+ }
422
+ return results;
423
+ }
424
+ /** Extract knowledge from a conversation exchange — stricter matching to reduce false positives */
425
+ export function extractKnowledge(userMessage, assistantResponse) {
426
+ const lower = userMessage.toLowerCase().trim();
427
+ // Skip short messages and questions — they rarely contain teachable facts
428
+ if (lower.length < 15 || lower.endsWith('?'))
429
+ return;
430
+ // Only extract from messages that are clearly declarative/directive
431
+ // Require explicit teaching verbs at the START of the message or clause
432
+ const teachPatterns = [
433
+ /^(?:remember|note that|keep in mind)\s+(.{10,200})/i,
434
+ /^(?:always|never)\s+(.{10,200})/i,
435
+ /^(?:i (?:always |)(?:prefer|like|want|use|need))\s+(.{10,200})/i,
436
+ /^(?:my\s+\w+\s+(?:is|are|uses?|runs?|has))\s+(.{5,200})/i,
437
+ /^(?:we use|our\s+\w+\s+(?:is|are|uses?))\s+(.{5,200})/i,
438
+ // Also match after explicit "btw" / "fyi" / "also"
439
+ /(?:btw|fyi|also)[,:]?\s+(?:my|our|we|i)\s+(.{10,200})/i,
440
+ ];
441
+ for (const pattern of teachPatterns) {
442
+ const match = lower.match(pattern);
443
+ if (match) {
444
+ const fact = match[0].charAt(0).toUpperCase() + match[0].slice(1);
445
+ const category = /(?:always|never|prefer|like|want)/.test(lower) ? 'preference' :
446
+ /(?:my|our|we)/.test(lower) ? 'context' : 'fact';
447
+ learnFact(fact, category, 'extracted');
448
+ return; // Only extract one fact per message to avoid noise
449
+ }
450
+ }
451
+ // Detect corrections — require explicit correction language
452
+ const correctionPrefixes = /^(?:no[,.]?\s+(?:it|that|you)|that'?s\s+(?:wrong|incorrect|not right)|actually[,]?\s+(?:it|you|that))/i;
453
+ if (correctionPrefixes.test(lower)) {
454
+ const correctionMatch = lower.match(/(?:no[,.]?\s+|actually[,]?\s+|instead[,]?\s+|should\s+(?:be|use)\s+)(.{10,200})/i);
455
+ if (correctionMatch) {
456
+ recordCorrection(userMessage, assistantResponse);
457
+ learnFact(correctionMatch[1], 'rule', 'extracted');
458
+ }
459
+ }
460
+ // Detect project-specific knowledge — only from explicit project declarations
461
+ const projectPattern = /^(?:this (?:project|repo|app)|the codebase|our stack)\s+(?:is|uses?|has|runs?)\s+(.{5,200})/i;
462
+ const projectMatch = lower.match(projectPattern);
463
+ if (projectMatch) {
464
+ learnFact(projectMatch[0], 'project', 'extracted');
465
+ }
466
+ }
467
+ let corrections = loadJSON(CORRECTIONS_FILE, []);
468
+ /** Record a user correction */
469
+ export function recordCorrection(userMessage, wrongResponse) {
470
+ const rule = userMessage.slice(0, 300);
471
+ // Deduplicate
472
+ const existing = corrections.find(c => normalizeIntent(c.userMessage) === normalizeIntent(userMessage));
473
+ if (existing) {
474
+ existing.occurrences++;
475
+ saveJSON(CORRECTIONS_FILE, corrections);
476
+ return;
477
+ }
478
+ corrections.push({
479
+ userMessage: userMessage.slice(0, 300),
480
+ wrongResponse: wrongResponse.slice(0, 300),
481
+ rule,
482
+ occurrences: 1,
483
+ created: new Date().toISOString(),
484
+ });
485
+ // Keep top 50 corrections
486
+ corrections.sort((a, b) => b.occurrences - a.occurrences);
487
+ corrections = corrections.slice(0, 50);
488
+ saveJSON(CORRECTIONS_FILE, corrections);
489
+ }
490
+ /** Get relevant corrections to avoid repeating mistakes */
491
+ export function getRelevantCorrections(message, max = 3) {
492
+ const msgWords = new Set(message.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 2));
493
+ return corrections
494
+ .map(c => {
495
+ const cWords = new Set(c.userMessage.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 2));
496
+ const overlap = [...msgWords].filter(w => cWords.has(w)).length;
497
+ const score = cWords.size > 0 ? overlap / cWords.size : 0;
498
+ return { correction: c, score };
499
+ })
500
+ .filter(s => s.score > 0.3)
501
+ .sort((a, b) => b.score - a.score)
502
+ .slice(0, max)
503
+ .map(s => s.correction);
504
+ }
505
+ let projects = loadJSON(PROJECTS_FILE, []);
506
+ /** Get or create project memory for current directory */
507
+ export function getProjectMemory(cwd) {
508
+ const project = projects.find(p => cwd.startsWith(p.path));
509
+ if (project) {
510
+ project.lastAccessed = new Date().toISOString();
511
+ saveJSON(PROJECTS_FILE, projects);
512
+ }
513
+ return project || null;
514
+ }
515
+ /** Record project information */
516
+ export function updateProjectMemory(cwd, data) {
517
+ let project = projects.find(p => p.path === cwd);
518
+ if (!project) {
519
+ project = {
520
+ path: cwd,
521
+ name: data.name || cwd.split('/').pop() || 'unknown',
522
+ stack: [],
523
+ frequentFiles: {},
524
+ notes: [],
525
+ lastAccessed: new Date().toISOString(),
526
+ };
527
+ projects.push(project);
528
+ }
529
+ if (data.name)
530
+ project.name = data.name;
531
+ if (data.stack) {
532
+ const existing = new Set(project.stack);
533
+ for (const s of data.stack) {
534
+ if (!existing.has(s)) {
535
+ project.stack.push(s);
536
+ existing.add(s);
537
+ }
538
+ }
539
+ project.stack = project.stack.slice(0, 20);
540
+ }
541
+ if (data.file) {
542
+ project.frequentFiles[data.file] = (project.frequentFiles[data.file] || 0) + 1;
543
+ // Keep top 30 files
544
+ const sorted = Object.entries(project.frequentFiles).sort((a, b) => b[1] - a[1]).slice(0, 30);
545
+ project.frequentFiles = Object.fromEntries(sorted);
546
+ }
547
+ if (data.note && !project.notes.includes(data.note)) {
548
+ project.notes.push(data.note);
549
+ project.notes = project.notes.slice(-50);
550
+ }
551
+ project.lastAccessed = new Date().toISOString();
552
+ // Keep top 20 projects
553
+ projects.sort((a, b) => new Date(b.lastAccessed).getTime() - new Date(a.lastAccessed).getTime());
554
+ projects = projects.slice(0, 20);
555
+ saveJSON(PROJECTS_FILE, projects);
556
+ }
557
+ // ═══ ENHANCED CONTEXT BUILDER — Uses all learning systems ═══════
558
+ /** Override the original buildLearningContext with enhanced version */
559
+ // Enhance the existing buildLearningContext
560
+ export function buildFullLearningContext(message, cwd) {
561
+ const parts = [];
562
+ // A. Pattern hint
563
+ const pattern = findPattern(message);
564
+ if (pattern) {
565
+ parts.push(`[Learned Pattern — ${pattern.hits}x success]`, `Tool sequence: ${pattern.toolSequence.join(' → ')}`);
566
+ }
567
+ // B. Relevant solutions
568
+ const relevant = findSolutions(message, 2);
569
+ if (relevant.length > 0) {
570
+ parts.push('[Cached Solutions]');
571
+ for (const s of relevant) {
572
+ parts.push(`Q: ${s.question}\nA: ${s.solution}`);
573
+ }
574
+ }
575
+ // C. Relevant knowledge — things the user has taught
576
+ const knowledgeEntries = findKnowledge(message, 4);
577
+ if (knowledgeEntries.length > 0) {
578
+ parts.push('[User Knowledge]');
579
+ for (const k of knowledgeEntries) {
580
+ const tag = k.source === 'user-taught' ? '(user said)' : '(learned)';
581
+ parts.push(`• ${k.fact} ${tag}`);
582
+ }
583
+ }
584
+ // D. Corrections — avoid repeating past mistakes
585
+ const relevantCorrections = getRelevantCorrections(message, 2);
586
+ if (relevantCorrections.length > 0) {
587
+ parts.push('[Past Corrections — avoid these mistakes]');
588
+ for (const c of relevantCorrections) {
589
+ parts.push(`• User corrected: "${c.rule}"`);
590
+ }
591
+ }
592
+ // E. User profile
593
+ if (profile.totalMessages > 3) {
594
+ const hints = [];
595
+ if (profile.techStack.length > 0) {
596
+ hints.push(`Stack: ${profile.techStack.join(', ')}`);
597
+ }
598
+ if (profile.responseStyle !== 'auto') {
599
+ hints.push(`Style: ${profile.responseStyle}`);
600
+ }
601
+ const topTask = Object.entries(profile.taskPatterns).sort((a, b) => b[1] - a[1])[0];
602
+ if (topTask && topTask[1] > 2) {
603
+ hints.push(`Common task: ${topTask[0]}`);
604
+ }
605
+ if (hints.length > 0) {
606
+ parts.push(`[User Profile] ${hints.join(' · ')}`);
607
+ }
608
+ }
609
+ // F. Project memory — if working in a known project
610
+ if (cwd) {
611
+ const project = getProjectMemory(cwd);
612
+ if (project) {
613
+ const projectHints = [`Project: ${project.name}`];
614
+ if (project.stack.length > 0)
615
+ projectHints.push(`Stack: ${project.stack.join(', ')}`);
616
+ if (project.notes.length > 0) {
617
+ projectHints.push('Notes:');
618
+ for (const note of project.notes.slice(-5)) {
619
+ projectHints.push(` • ${note}`);
620
+ }
621
+ }
622
+ const topFiles = Object.entries(project.frequentFiles)
623
+ .sort((a, b) => b[1] - a[1])
624
+ .slice(0, 5)
625
+ .map(([f]) => f);
626
+ if (topFiles.length > 0) {
627
+ projectHints.push(`Frequent files: ${topFiles.join(', ')}`);
628
+ }
629
+ parts.push(`[Project Context]\n${projectHints.join('\n')}`);
630
+ }
631
+ }
632
+ return parts.length > 0 ? parts.join('\n\n') : '';
633
+ }
634
+ // ═══ 9. CONVERSATION LEARNING — Post-interaction extraction ═════
635
+ // Called after each exchange to extract and store learnings.
636
+ export function learnFromExchange(userMessage, assistantResponse, toolsUsed, cwd) {
637
+ // Extract knowledge from what the user said
638
+ extractKnowledge(userMessage, assistantResponse);
639
+ // Track file usage in project memory
640
+ if (cwd && toolsUsed.length > 0) {
641
+ const fileTools = ['read_file', 'write_file', 'edit_file', 'multi_file_write'];
642
+ // Try to extract file paths from tool names (simplified)
643
+ for (const tool of toolsUsed) {
644
+ if (fileTools.includes(tool)) {
645
+ // The actual file paths would need to come from tool args — for now just track the tools
646
+ updateProjectMemory(cwd, { stack: extractKeywords(userMessage) });
647
+ }
648
+ }
649
+ }
650
+ // Detect style preference from response feedback
651
+ const lower = userMessage.toLowerCase();
652
+ if (/(?:too (?:long|verbose|detailed)|shorter|tldr|brief)/i.test(lower)) {
653
+ profile.responseStyle = 'concise';
654
+ saveJSON(PROFILE_FILE, profile);
655
+ }
656
+ if (/(?:more detail|elaborate|explain more|too short|too brief)/i.test(lower)) {
657
+ profile.responseStyle = 'detailed';
658
+ saveJSON(PROFILE_FILE, profile);
659
+ }
660
+ // Save periodic stats
661
+ saveJSON(KNOWLEDGE_FILE, knowledge);
662
+ }
663
+ // ═══ 10. LEARNING STATS — Extended ══════════════════════════════
664
+ // ═══ 11. SELF-TRAINING — Periodic knowledge review & synthesis ════
665
+ // kbot reviews its own knowledge base, prunes stale entries,
666
+ // synthesizes cross-pattern insights, and optimizes the learning engine.
667
+ const TRAINING_FILE = join(LEARN_DIR, 'training-log.json');
668
+ let trainingLog = loadJSON(TRAINING_FILE, {
669
+ lastRun: '',
670
+ runsTotal: 0,
671
+ entriesPruned: 0,
672
+ insightsSynthesized: 0,
673
+ patternsOptimized: 0,
674
+ });
675
+ /** Run self-training: prune stale knowledge, optimize patterns, synthesize insights */
676
+ export function selfTrain() {
677
+ let pruned = 0;
678
+ let optimized = 0;
679
+ let synthesized = 0;
680
+ const summaryParts = [];
681
+ // ── A. Prune stale patterns ──
682
+ // Remove patterns with low success rate or no recent hits
683
+ const now = Date.now();
684
+ const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
685
+ const beforePatterns = patterns.length;
686
+ patterns = patterns.filter(p => {
687
+ const lastUsed = new Date(p.lastUsed).getTime();
688
+ // Keep if: recent, high success rate, or frequently used
689
+ if (lastUsed > thirtyDaysAgo)
690
+ return true;
691
+ if (p.successRate > 0.8 && p.hits > 5)
692
+ return true;
693
+ if (p.hits > 10)
694
+ return true;
695
+ pruned++;
696
+ return false;
697
+ });
698
+ if (pruned > 0) {
699
+ saveJSON(PATTERNS_FILE, patterns);
700
+ summaryParts.push(`Pruned ${pruned} stale patterns (${beforePatterns} → ${patterns.length})`);
701
+ }
702
+ // ── B. Prune low-confidence knowledge ──
703
+ const beforeKnowledge = knowledge.length;
704
+ knowledge = knowledge.filter(k => {
705
+ // Keep all user-taught facts
706
+ if (k.source === 'user-taught')
707
+ return true;
708
+ // Keep high-confidence or frequently referenced
709
+ if (k.confidence > 0.5 && k.references > 0)
710
+ return true;
711
+ // Keep recent entries (< 7 days)
712
+ const created = new Date(k.created).getTime();
713
+ if (created > now - 7 * 24 * 60 * 60 * 1000)
714
+ return true;
715
+ pruned++;
716
+ return false;
717
+ });
718
+ if (knowledge.length < beforeKnowledge) {
719
+ saveJSON(KNOWLEDGE_FILE, knowledge);
720
+ summaryParts.push(`Pruned ${beforeKnowledge - knowledge.length} low-confidence knowledge entries`);
721
+ }
722
+ // ── C. Optimize patterns — merge similar ones ──
723
+ const mergedPatterns = new Map();
724
+ for (const p of patterns) {
725
+ const key = p.toolSequence.join(',');
726
+ const existing = mergedPatterns.get(key);
727
+ if (existing && p.intent !== existing.intent) {
728
+ // Same tool sequence, different intent — merge keywords
729
+ existing.keywords = [...new Set([...existing.keywords, ...p.keywords])];
730
+ existing.hits += p.hits;
731
+ existing.successRate = (existing.successRate + p.successRate) / 2;
732
+ optimized++;
733
+ }
734
+ else {
735
+ mergedPatterns.set(key, { ...p });
736
+ }
737
+ }
738
+ if (optimized > 0) {
739
+ patterns = Array.from(mergedPatterns.values());
740
+ saveJSON(PATTERNS_FILE, patterns);
741
+ summaryParts.push(`Merged ${optimized} redundant patterns`);
742
+ }
743
+ // ── D. Synthesize cross-pattern insights ──
744
+ // Find common tool sequences across patterns to identify power workflows
745
+ const toolFrequency = {};
746
+ for (const p of patterns) {
747
+ for (const tool of p.toolSequence) {
748
+ toolFrequency[tool] = (toolFrequency[tool] || 0) + p.hits;
749
+ }
750
+ }
751
+ const topTools = Object.entries(toolFrequency)
752
+ .sort((a, b) => b[1] - a[1])
753
+ .slice(0, 5);
754
+ if (topTools.length > 0) {
755
+ const insight = `Most effective tools: ${topTools.map(([t, n]) => `${t}(${n}x)`).join(', ')}`;
756
+ const existingInsight = knowledge.find(k => k.fact.startsWith('Most effective tools:'));
757
+ if (existingInsight) {
758
+ existingInsight.fact = insight;
759
+ existingInsight.lastUsed = new Date().toISOString();
760
+ }
761
+ else {
762
+ learnFact(insight, 'context', 'observed');
763
+ synthesized++;
764
+ }
765
+ }
766
+ // Synthesize user task preference insights
767
+ const topTasks = Object.entries(profile.taskPatterns)
768
+ .sort((a, b) => b[1] - a[1])
769
+ .slice(0, 3);
770
+ if (topTasks.length > 0 && profile.totalMessages > 10) {
771
+ const insight = `User primarily does: ${topTasks.map(([t, n]) => `${t}(${n}x)`).join(', ')}`;
772
+ const existing = knowledge.find(k => k.fact.startsWith('User primarily does:'));
773
+ if (existing) {
774
+ existing.fact = insight;
775
+ existing.lastUsed = new Date().toISOString();
776
+ }
777
+ else {
778
+ learnFact(insight, 'context', 'observed');
779
+ synthesized++;
780
+ }
781
+ }
782
+ // Synthesize solution success patterns
783
+ const highConfSolutions = solutions.filter(s => s.confidence > 0.9 && s.reuses > 2);
784
+ if (highConfSolutions.length > 0) {
785
+ summaryParts.push(`${highConfSolutions.length} battle-tested solutions (>90% confidence, 2+ reuses)`);
786
+ }
787
+ if (synthesized > 0) {
788
+ saveJSON(KNOWLEDGE_FILE, knowledge);
789
+ summaryParts.push(`Synthesized ${synthesized} new insights`);
790
+ }
791
+ // ── E. Update training log ──
792
+ trainingLog.lastRun = new Date().toISOString();
793
+ trainingLog.runsTotal++;
794
+ trainingLog.entriesPruned += pruned;
795
+ trainingLog.insightsSynthesized += synthesized;
796
+ trainingLog.patternsOptimized += optimized;
797
+ saveJSON(TRAINING_FILE, trainingLog);
798
+ const summary = summaryParts.length > 0
799
+ ? summaryParts.join('\n')
800
+ : 'Knowledge base is clean. No changes needed.';
801
+ return { pruned, optimized, synthesized, summary };
802
+ }
803
+ /** Check if self-training should run (auto-trigger every 50 messages) */
804
+ export function shouldAutoTrain() {
805
+ if (!trainingLog.lastRun)
806
+ return profile.totalMessages >= 20;
807
+ const lastRun = new Date(trainingLog.lastRun).getTime();
808
+ const hoursSinceLastRun = (Date.now() - lastRun) / (1000 * 60 * 60);
809
+ // Auto-train if: > 24 hours since last run AND > 20 messages since
810
+ return hoursSinceLastRun > 24 && profile.totalMessages % 50 === 0;
811
+ }
812
+ /** Get training log for display */
813
+ export function getTrainingLog() {
814
+ return trainingLog;
815
+ }
816
+ export function getExtendedStats() {
817
+ const base = getStats();
818
+ return {
819
+ ...base,
820
+ knowledgeCount: knowledge.length,
821
+ correctionsCount: corrections.length,
822
+ projectsCount: projects.length,
823
+ topKnowledge: knowledge
824
+ .sort((a, b) => b.references - a.references)
825
+ .slice(0, 5)
826
+ .map(k => k.fact.slice(0, 80)),
827
+ };
828
+ }
829
+ //# sourceMappingURL=learning.js.map