@kernel.chat/kbot 3.63.0 → 3.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,461 @@
1
+ // kbot Memory Scanner — Passive In-Session Memory Detection
2
+ //
3
+ // The dream engine consolidates AFTER sessions end. This fills the gap:
4
+ // a passive scanner that watches conversation turns DURING a session and
5
+ // detects "memory-worthy" moments in real time.
6
+ //
7
+ // How it works:
8
+ // 1. Hook into addTurn() from memory.ts — observe every turn
9
+ // 2. After every N turns (default 5), scan the recent window
10
+ // 3. Use keyword + context windows (NOT regex sentiment — we're better than that)
11
+ // 4. When a memory-worthy moment is found, auto-save via the memory_save tool
12
+ // 5. Debounce and dedup to avoid noise
13
+ //
14
+ // Detection categories:
15
+ // - Corrections: "no, I meant...", "actually...", "don't do that"
16
+ // - Preferences: "I prefer...", "always use...", "never..."
17
+ // - Project facts: "the deadline is...", "we're using...", "the API key is in..."
18
+ // - Emotional: "this is frustrating", "perfect!", "exactly what I wanted"
19
+ //
20
+ // Storage: Scanner state persists to ~/.kbot/memory/scanner-state.json
21
+ // Memories saved via the memory_save tool (same as agent-initiated saves)
22
+ import { homedir } from 'node:os';
23
+ import { join } from 'node:path';
24
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
25
+ import { getHistory } from './memory.js';
26
+ // ── Constants ──
27
+ const SCANNER_STATE_FILE = join(homedir(), '.kbot', 'memory', 'scanner-state.json');
28
+ const SCAN_INTERVAL = 5; // Scan every N turns
29
+ const CONTEXT_WINDOW = 6; // Look at the last N turns when scanning
30
+ const DEDUP_WINDOW_MS = 300_000; // 5 min — don't save duplicate memories within this window
31
+ const MAX_MEMORIES_PER_SESSION = 20; // Safety cap — don't flood memory store
32
+ // Correction signals — user is fixing a misunderstanding
33
+ const CORRECTION_PATTERNS = {
34
+ triggers: [
35
+ 'no, i meant', 'no i meant', 'actually,', 'actually ', "that's not what i",
36
+ "don't do that", 'dont do that', 'not what i asked', 'i said',
37
+ 'what i meant was', 'let me clarify', 'to clarify', 'i was referring to',
38
+ 'you misunderstood', 'that\'s not right', "that's wrong", 'incorrect',
39
+ 'no, use', 'no, it should', 'wrong approach', 'not like that',
40
+ 'i meant', 'what i want is', 'stop doing',
41
+ ],
42
+ validators: [
43
+ // Validator: correction follows an assistant message (there's something to correct)
44
+ (_turn, context) => {
45
+ const prevAssistant = [...context].reverse().find(t => t.role === 'assistant');
46
+ if (!prevAssistant)
47
+ return null;
48
+ return 'correction after assistant response';
49
+ },
50
+ ],
51
+ kind: 'correction',
52
+ category: 'pattern',
53
+ };
54
+ // Preference signals — user states how they like things done
55
+ const PREFERENCE_PATTERNS = {
56
+ triggers: [
57
+ 'i prefer', 'i always', 'i never', 'always use', 'never use',
58
+ 'i like to', "i don't like", 'i dont like', 'my preference is',
59
+ 'i usually', 'i tend to', 'please always', 'please never',
60
+ 'from now on', 'going forward', "let's stick with", 'lets stick with',
61
+ 'i want you to always', 'i want you to never', 'keep using',
62
+ 'stop using', 'switch to', 'use this instead', 'my style is',
63
+ 'the way i like it', 'my convention is',
64
+ ],
65
+ validators: [
66
+ // Validator: message is long enough to contain a real preference (not just "I prefer")
67
+ (turn) => {
68
+ const words = turn.content.trim().split(/\s+/);
69
+ if (words.length >= 5)
70
+ return 'substantive preference statement';
71
+ return null;
72
+ },
73
+ ],
74
+ kind: 'preference',
75
+ category: 'preference',
76
+ };
77
+ // Project fact signals — user shares project context
78
+ const PROJECT_FACT_PATTERNS = {
79
+ triggers: [
80
+ 'the deadline is', 'deadline is', 'due date is', 'due by',
81
+ "we're using", 'we are using', "we're running", 'we use',
82
+ 'the api key is in', 'api key is stored', 'credentials are in',
83
+ 'the database is', 'our database', 'the repo is', 'our repo',
84
+ 'the stack is', 'our stack', 'tech stack is',
85
+ 'we deploy to', 'deployed on', 'hosted on', 'runs on',
86
+ 'the config is', 'config file is', 'settings are in',
87
+ 'the port is', 'it runs on port', 'the endpoint is',
88
+ 'the team uses', 'our team', 'my team',
89
+ 'the project is', 'this project', 'the codebase',
90
+ 'the convention is', 'our convention', 'the rule is',
91
+ 'we follow', 'the standard is', 'our standard',
92
+ 'the branch is', 'main branch is', 'we branch from',
93
+ ],
94
+ validators: [
95
+ // Validator: contains specific details (numbers, paths, names)
96
+ (turn) => {
97
+ const content = turn.content;
98
+ // Check for specifics: paths, URLs, versions, port numbers, dates
99
+ const hasSpecifics = /(?:\/[\w.-]+\/|https?:\/\/|v?\d+\.\d+|port\s+\d+|\d{4}-\d{2})/i.test(content);
100
+ if (hasSpecifics)
101
+ return 'contains specific project details';
102
+ // Check for substantial content (at least a sentence)
103
+ if (content.trim().split(/\s+/).length >= 8)
104
+ return 'substantive project statement';
105
+ return null;
106
+ },
107
+ ],
108
+ kind: 'project_fact',
109
+ category: 'fact',
110
+ };
111
+ // Emotional signals — user's satisfaction/frustration
112
+ const EMOTIONAL_PATTERNS = {
113
+ triggers: [
114
+ 'this is frustrating', 'so frustrating', 'ugh', 'annoying',
115
+ 'perfect!', 'exactly what i wanted', 'exactly right', 'exactly!',
116
+ 'love it', 'great job', 'well done', 'nice work', 'nailed it',
117
+ 'this is amazing', 'this is terrible', 'this sucks', 'hate this',
118
+ 'finally!', 'thank god', 'thank you!', 'thanks!',
119
+ 'this is exactly', "that's exactly", "that's perfect",
120
+ 'much better', 'way better', 'huge improvement',
121
+ 'this keeps happening', 'same issue again', 'broken again',
122
+ ],
123
+ validators: [
124
+ // Validator: emotional signals are only worth saving if they contain
125
+ // actionable context (what was good/bad)
126
+ (turn, context) => {
127
+ // Positive signal after assistant response = something worked well
128
+ const prevAssistant = [...context].reverse().find(t => t.role === 'assistant');
129
+ if (prevAssistant)
130
+ return 'emotional reaction to assistant output';
131
+ return null;
132
+ },
133
+ ],
134
+ kind: 'emotional',
135
+ category: 'pattern',
136
+ };
137
+ const ALL_PATTERNS = [
138
+ CORRECTION_PATTERNS,
139
+ PREFERENCE_PATTERNS,
140
+ PROJECT_FACT_PATTERNS,
141
+ EMOTIONAL_PATTERNS,
142
+ ];
143
+ // ── Scanner State ──
144
+ let enabled = true;
145
+ let turnsObserved = 0;
146
+ let scansPerformed = 0;
147
+ let momentsDetected = 0;
148
+ let memoriesSaved = 0;
149
+ let sessionStart = new Date().toISOString();
150
+ const byKind = {
151
+ correction: 0,
152
+ preference: 0,
153
+ project_fact: 0,
154
+ emotional: 0,
155
+ };
156
+ const recentDetections = [];
157
+ const recentSavedKeys = new Map(); // key → timestamp, for dedup
158
+ let turnCountSinceLastScan = 0;
159
+ let hooked = false;
160
+ // ── Persistent State ──
161
+ function ensureDir() {
162
+ const dir = join(homedir(), '.kbot', 'memory');
163
+ if (!existsSync(dir))
164
+ mkdirSync(dir, { recursive: true });
165
+ }
166
+ function loadScannerState() {
167
+ ensureDir();
168
+ const defaults = {
169
+ totalScans: 0,
170
+ totalDetections: 0,
171
+ totalSaved: 0,
172
+ lastScan: null,
173
+ cumulativeByKind: { correction: 0, preference: 0, project_fact: 0, emotional: 0 },
174
+ };
175
+ if (!existsSync(SCANNER_STATE_FILE))
176
+ return defaults;
177
+ try {
178
+ return { ...defaults, ...JSON.parse(readFileSync(SCANNER_STATE_FILE, 'utf-8')) };
179
+ }
180
+ catch {
181
+ return defaults;
182
+ }
183
+ }
184
+ function saveScannerState() {
185
+ ensureDir();
186
+ const state = loadScannerState();
187
+ state.totalScans += scansPerformed;
188
+ state.totalDetections += momentsDetected;
189
+ state.totalSaved += memoriesSaved;
190
+ state.lastScan = new Date().toISOString();
191
+ for (const kind of Object.keys(byKind)) {
192
+ state.cumulativeByKind[kind] = (state.cumulativeByKind[kind] || 0) + byKind[kind];
193
+ }
194
+ try {
195
+ writeFileSync(SCANNER_STATE_FILE, JSON.stringify(state, null, 2));
196
+ }
197
+ catch {
198
+ // Non-critical — state can be rebuilt
199
+ }
200
+ }
201
+ // ── Detection Engine ──
202
+ /** Extract a clean, saveable memory from a detected signal */
203
+ function extractMemoryContent(turn, context, kind) {
204
+ const userMsg = turn.content.trim();
205
+ // For corrections, include what was corrected
206
+ if (kind === 'correction') {
207
+ const prevAssistant = [...context].reverse().find(t => t.role === 'assistant');
208
+ const prevSnippet = prevAssistant ? prevAssistant.content.slice(0, 80) : '';
209
+ const content = prevSnippet
210
+ ? `User corrected: "${prevSnippet}..." → "${userMsg.slice(0, 200)}"`
211
+ : `User correction: ${userMsg.slice(0, 250)}`;
212
+ const key = `correction-${slugify(userMsg.slice(0, 40))}`;
213
+ return { content, key };
214
+ }
215
+ // For preferences, capture the full preference statement
216
+ if (kind === 'preference') {
217
+ const content = `User preference: ${userMsg.slice(0, 300)}`;
218
+ const key = `pref-${slugify(userMsg.slice(0, 40))}`;
219
+ return { content, key };
220
+ }
221
+ // For project facts, capture the factual statement
222
+ if (kind === 'project_fact') {
223
+ const content = `Project fact: ${userMsg.slice(0, 300)}`;
224
+ const key = `fact-${slugify(userMsg.slice(0, 40))}`;
225
+ return { content, key };
226
+ }
227
+ // For emotional signals, note what provoked the reaction
228
+ const prevAssistant = [...context].reverse().find(t => t.role === 'assistant');
229
+ const prevSnippet = prevAssistant ? prevAssistant.content.slice(0, 100) : '';
230
+ const sentiment = detectSentimentDirection(userMsg);
231
+ const content = prevSnippet
232
+ ? `User ${sentiment} reaction to: "${prevSnippet}..." — "${userMsg.slice(0, 150)}"`
233
+ : `User ${sentiment} reaction: ${userMsg.slice(0, 200)}`;
234
+ const key = `emotion-${sentiment}-${slugify(userMsg.slice(0, 30))}`;
235
+ return { content, key };
236
+ }
237
+ /** Determine if the emotional signal is positive or negative */
238
+ function detectSentimentDirection(text) {
239
+ const lower = text.toLowerCase();
240
+ const positiveWords = ['perfect', 'exactly', 'love', 'great', 'nice', 'nailed', 'amazing', 'better', 'improvement', 'finally', 'thank'];
241
+ const negativeWords = ['frustrating', 'annoying', 'terrible', 'sucks', 'hate', 'broken', 'ugh', 'same issue'];
242
+ let posScore = 0;
243
+ let negScore = 0;
244
+ for (const w of positiveWords) {
245
+ if (lower.includes(w))
246
+ posScore++;
247
+ }
248
+ for (const w of negativeWords) {
249
+ if (lower.includes(w))
250
+ negScore++;
251
+ }
252
+ return posScore >= negScore ? 'positive' : 'negative';
253
+ }
254
+ /** Create a filesystem-safe slug from text */
255
+ function slugify(text) {
256
+ return text
257
+ .toLowerCase()
258
+ .replace(/[^a-z0-9]+/g, '-')
259
+ .replace(/^-|-$/g, '')
260
+ .slice(0, 60);
261
+ }
262
+ /** Check if a key was recently saved (dedup within the window) */
263
+ function isRecentlySaved(key) {
264
+ const savedAt = recentSavedKeys.get(key);
265
+ if (!savedAt)
266
+ return false;
267
+ return (Date.now() - savedAt) < DEDUP_WINDOW_MS;
268
+ }
269
+ /** Scan a window of conversation turns for memory-worthy signals */
270
+ function scanTurns(sessionId) {
271
+ const history = getHistory(sessionId);
272
+ if (history.length < 2)
273
+ return []; // Need at least one exchange
274
+ // Look at the recent context window
275
+ const windowStart = Math.max(0, history.length - CONTEXT_WINDOW);
276
+ const window = history.slice(windowStart);
277
+ const detected = [];
278
+ // Only scan user turns (assistant turns don't contain user memories)
279
+ for (let i = 0; i < window.length; i++) {
280
+ const turn = window[i];
281
+ if (turn.role !== 'user')
282
+ continue;
283
+ const lowerContent = turn.content.toLowerCase();
284
+ for (const pattern of ALL_PATTERNS) {
285
+ // Phase 1: Keyword trigger check — does the message contain any trigger phrase?
286
+ const triggered = pattern.triggers.some(trigger => lowerContent.includes(trigger));
287
+ if (!triggered)
288
+ continue;
289
+ // Phase 2: Context validation — does the surrounding context confirm the signal?
290
+ const contextBefore = window.slice(0, i);
291
+ let validated = false;
292
+ for (const validator of pattern.validators) {
293
+ const result = validator(turn, contextBefore);
294
+ if (result !== null) {
295
+ validated = true;
296
+ break;
297
+ }
298
+ }
299
+ if (!validated)
300
+ continue;
301
+ // Phase 3: Extract memory content
302
+ const { content, key } = extractMemoryContent(turn, contextBefore, pattern.kind);
303
+ // Phase 4: Dedup — skip if we already saved this key recently
304
+ if (isRecentlySaved(key))
305
+ continue;
306
+ // Phase 5: Confidence scoring
307
+ // More triggers matched = higher confidence
308
+ const triggerCount = pattern.triggers.filter(t => lowerContent.includes(t)).length;
309
+ const confidence = Math.min(0.5 + triggerCount * 0.15, 0.95);
310
+ detected.push({
311
+ kind: pattern.kind,
312
+ content,
313
+ key,
314
+ category: pattern.category,
315
+ confidence,
316
+ turnIndex: windowStart + i,
317
+ detectedAt: new Date().toISOString(),
318
+ });
319
+ }
320
+ }
321
+ return detected;
322
+ }
323
+ // ── Memory Saving ──
324
+ /** Save a detected memory via the memory tool filesystem (bypasses tool execution) */
325
+ function saveDetectedMemory(memory) {
326
+ try {
327
+ const memDir = join(homedir(), '.kbot', 'memory', memory.category);
328
+ if (!existsSync(memDir))
329
+ mkdirSync(memDir, { recursive: true });
330
+ const sanitizedKey = memory.key
331
+ .toLowerCase()
332
+ .replace(/[^a-z0-9_-]/g, '-')
333
+ .replace(/-+/g, '-')
334
+ .replace(/^-|-$/g, '')
335
+ .slice(0, 128);
336
+ const filePath = join(memDir, `${sanitizedKey}.json`);
337
+ const now = new Date().toISOString();
338
+ // Check if memory already exists — update instead of overwrite
339
+ let existing = null;
340
+ if (existsSync(filePath)) {
341
+ try {
342
+ existing = JSON.parse(readFileSync(filePath, 'utf-8'));
343
+ }
344
+ catch {
345
+ existing = null;
346
+ }
347
+ }
348
+ const entry = {
349
+ key: sanitizedKey,
350
+ content: memory.content,
351
+ category: memory.category,
352
+ created_at: existing?.created_at || now,
353
+ modified_at: now,
354
+ access_count: (existing?.access_count || 0) + 1,
355
+ source: 'memory-scanner',
356
+ signal_kind: memory.kind,
357
+ confidence: memory.confidence,
358
+ };
359
+ writeFileSync(filePath, JSON.stringify(entry, null, 2));
360
+ recentSavedKeys.set(memory.key, Date.now());
361
+ return true;
362
+ }
363
+ catch {
364
+ return false;
365
+ }
366
+ }
367
+ // ── Turn Observer ──
368
+ /** Called after every addTurn — this is the scanner's heartbeat */
369
+ function onTurnAdded(turn, sessionId) {
370
+ if (!enabled)
371
+ return;
372
+ turnsObserved++;
373
+ turnCountSinceLastScan++;
374
+ // Only scan on the interval
375
+ if (turnCountSinceLastScan < SCAN_INTERVAL)
376
+ return;
377
+ turnCountSinceLastScan = 0;
378
+ // Safety cap
379
+ if (memoriesSaved >= MAX_MEMORIES_PER_SESSION)
380
+ return;
381
+ // Run detection (synchronous — fast keyword matching, no I/O)
382
+ const detected = scanTurns(sessionId);
383
+ scansPerformed++;
384
+ for (const memory of detected) {
385
+ momentsDetected++;
386
+ byKind[memory.kind]++;
387
+ // Save the memory
388
+ const saved = saveDetectedMemory(memory);
389
+ if (saved) {
390
+ memoriesSaved++;
391
+ // Keep recent detections list bounded
392
+ recentDetections.push(memory);
393
+ if (recentDetections.length > 10)
394
+ recentDetections.shift();
395
+ }
396
+ }
397
+ }
398
+ // ── Monkey-Patch Hook ──
399
+ //
400
+ // We wrap the original addTurn() to observe turns without modifying memory.ts.
401
+ // This is the same pattern used by learning.ts's async extraction.
402
+ /** Install the scanner hook on addTurn. Idempotent. */
403
+ function installHook() {
404
+ if (hooked)
405
+ return;
406
+ // We can't actually monkey-patch an imported function binding in ESM.
407
+ // Instead, we expose a notifyTurn() function that the agent loop calls.
408
+ // See: agent.ts calls addTurn() then notifyTurn() for scanner awareness.
409
+ hooked = true;
410
+ }
411
+ // ── Public API ──
412
+ /** Notify the scanner that a turn was added. Call after addTurn(). */
413
+ export function notifyTurn(turn, sessionId = 'default') {
414
+ onTurnAdded(turn, sessionId);
415
+ }
416
+ /** Start the memory scanner for the current session */
417
+ export function startMemoryScanner() {
418
+ enabled = true;
419
+ turnsObserved = 0;
420
+ scansPerformed = 0;
421
+ momentsDetected = 0;
422
+ memoriesSaved = 0;
423
+ turnCountSinceLastScan = 0;
424
+ sessionStart = new Date().toISOString();
425
+ byKind.correction = 0;
426
+ byKind.preference = 0;
427
+ byKind.project_fact = 0;
428
+ byKind.emotional = 0;
429
+ recentDetections.length = 0;
430
+ recentSavedKeys.clear();
431
+ installHook();
432
+ }
433
+ /** Stop the memory scanner and persist stats */
434
+ export function stopMemoryScanner() {
435
+ if (memoriesSaved > 0 || scansPerformed > 0) {
436
+ saveScannerState();
437
+ }
438
+ enabled = false;
439
+ }
440
+ /** Get current scanner stats */
441
+ export function getMemoryScannerStats() {
442
+ return {
443
+ enabled,
444
+ turnsObserved,
445
+ scansPerformed,
446
+ momentsDetected,
447
+ memoriesSaved,
448
+ byKind: { ...byKind },
449
+ recentDetections: [...recentDetections],
450
+ sessionStart,
451
+ };
452
+ }
453
+ /** Check if the scanner is currently enabled */
454
+ export function isScannerEnabled() {
455
+ return enabled;
456
+ }
457
+ /** Get cumulative stats from disk (across all sessions) */
458
+ export function getCumulativeScannerStats() {
459
+ return loadScannerState();
460
+ }
461
+ //# sourceMappingURL=memory-scanner.js.map
@@ -0,0 +1,2 @@
1
+ export declare function registerBuddyTools(): void;
2
+ //# sourceMappingURL=buddy-tools.d.ts.map
@@ -0,0 +1,63 @@
1
+ // kbot Buddy Tools — Interact with your terminal companion
2
+ //
3
+ // Two tools:
4
+ // buddy_status — Show buddy name, species, mood, and sprite
5
+ // buddy_rename — Give your buddy a custom name (persisted to ~/.kbot/buddy.json)
6
+ import { registerTool } from './index.js';
7
+ import { getBuddy, getBuddySprite, getBuddyGreeting, formatBuddyStatus, renameBuddy, } from '../buddy.js';
8
+ const VALID_MOODS = ['idle', 'thinking', 'success', 'error', 'learning'];
9
+ export function registerBuddyTools() {
10
+ registerTool({
11
+ name: 'buddy_status',
12
+ description: 'Show your terminal buddy — its name, species, current mood, and ASCII sprite. Optionally preview a specific mood.',
13
+ parameters: {
14
+ mood: {
15
+ type: 'string',
16
+ description: 'Preview a specific mood: idle, thinking, success, error, learning. Defaults to current mood.',
17
+ },
18
+ },
19
+ tier: 'free',
20
+ async execute(args) {
21
+ const buddy = getBuddy();
22
+ const mood = args.mood ? String(args.mood) : undefined;
23
+ if (mood && !VALID_MOODS.includes(mood)) {
24
+ return `Unknown mood "${mood}". Valid moods: ${VALID_MOODS.join(', ')}`;
25
+ }
26
+ const sprite = getBuddySprite(mood).join('\n');
27
+ const greeting = getBuddyGreeting();
28
+ return [
29
+ `Name: ${buddy.name}`,
30
+ `Species: ${buddy.species}`,
31
+ `Mood: ${mood || buddy.mood}`,
32
+ '',
33
+ sprite,
34
+ '',
35
+ `"${greeting}"`,
36
+ ].join('\n');
37
+ },
38
+ });
39
+ registerTool({
40
+ name: 'buddy_rename',
41
+ description: 'Give your terminal buddy a custom name. The name is persisted to ~/.kbot/buddy.json.',
42
+ parameters: {
43
+ name: {
44
+ type: 'string',
45
+ description: 'The new name for your buddy',
46
+ required: true,
47
+ },
48
+ },
49
+ tier: 'free',
50
+ async execute(args) {
51
+ const newName = String(args.name).trim();
52
+ if (!newName)
53
+ return 'Error: name cannot be empty.';
54
+ if (newName.length > 32)
55
+ return 'Error: name must be 32 characters or fewer.';
56
+ const oldBuddy = getBuddy();
57
+ const oldName = oldBuddy.name;
58
+ renameBuddy(newName);
59
+ return formatBuddyStatus(`${oldName} is now ${newName}!`);
60
+ },
61
+ });
62
+ }
63
+ //# sourceMappingURL=buddy-tools.js.map
@@ -299,6 +299,9 @@ const LAZY_MODULE_IMPORTS = [
299
299
  { path: './dj-set-builder.js', registerFn: 'registerDjSetBuilderTools' },
300
300
  { path: './serum2-preset.js', registerFn: 'registerSerum2PresetTools' },
301
301
  { path: './dream-tools.js', registerFn: 'registerDreamTools' },
302
+ { path: './memory-scanner-tools.js', registerFn: 'registerMemoryScannerTools' },
303
+ { path: './buddy-tools.js', registerFn: 'registerBuddyTools' },
304
+ { path: './voice-input-tools.js', registerFn: 'registerVoiceInputTools' },
302
305
  ];
303
306
  /** Track whether lazy tools have been registered */
304
307
  let lazyToolsRegistered = false;
@@ -0,0 +1,2 @@
1
+ export declare function registerMemoryScannerTools(): void;
2
+ //# sourceMappingURL=memory-scanner-tools.d.ts.map
@@ -0,0 +1,87 @@
1
+ // kbot Memory Scanner Tools — Agent-accessible controls for the passive scanner
2
+ //
3
+ // Two tools:
4
+ // - memory_scan_status: Show scanner stats (moments detected, memories saved, etc.)
5
+ // - memory_scan_toggle: Enable/disable the passive scanner
6
+ import { registerTool } from './index.js';
7
+ import { getMemoryScannerStats, getCumulativeScannerStats, startMemoryScanner, stopMemoryScanner, isScannerEnabled, } from '../memory-scanner.js';
8
+ export function registerMemoryScannerTools() {
9
+ // ── memory_scan_status ──
10
+ registerTool({
11
+ name: 'memory_scan_status',
12
+ description: 'Show memory scanner status — how many turns observed, scans performed, memory-worthy moments detected, memories saved, and breakdown by signal kind (corrections, preferences, project facts, emotional). Includes cumulative stats across all sessions.',
13
+ parameters: {},
14
+ tier: 'free',
15
+ timeout: 5_000,
16
+ async execute() {
17
+ const stats = getMemoryScannerStats();
18
+ const cumulative = getCumulativeScannerStats();
19
+ const lines = [
20
+ 'Memory Scanner Status',
21
+ '═════════════════════',
22
+ `State: ${stats.enabled ? 'ACTIVE' : 'PAUSED'}`,
23
+ `Session start: ${stats.sessionStart.split('T')[0]} ${stats.sessionStart.split('T')[1]?.slice(0, 8) || ''}`,
24
+ '',
25
+ '── This Session ──',
26
+ `Turns observed: ${stats.turnsObserved}`,
27
+ `Scans performed: ${stats.scansPerformed}`,
28
+ `Moments detected: ${stats.momentsDetected}`,
29
+ `Memories saved: ${stats.memoriesSaved}`,
30
+ '',
31
+ 'Signal breakdown:',
32
+ ` Corrections: ${stats.byKind.correction}`,
33
+ ` Preferences: ${stats.byKind.preference}`,
34
+ ` Project facts: ${stats.byKind.project_fact}`,
35
+ ` Emotional: ${stats.byKind.emotional}`,
36
+ ];
37
+ if (stats.recentDetections.length > 0) {
38
+ lines.push('', 'Recent detections:');
39
+ for (const d of stats.recentDetections.slice(-5)) {
40
+ const time = d.detectedAt.split('T')[1]?.slice(0, 8) || '';
41
+ lines.push(` [${time}] [${d.kind}] (${Math.round(d.confidence * 100)}%) ${d.content.slice(0, 100)}${d.content.length > 100 ? '...' : ''}`);
42
+ }
43
+ }
44
+ lines.push('', '── Cumulative (all sessions) ──', `Total scans: ${cumulative.totalScans + stats.scansPerformed}`, `Total detections: ${cumulative.totalDetections + stats.momentsDetected}`, `Total saved: ${cumulative.totalSaved + stats.memoriesSaved}`, `Last scan: ${cumulative.lastScan || 'this session'}`, '', 'Cumulative by kind:', ` Corrections: ${(cumulative.cumulativeByKind.correction || 0) + stats.byKind.correction}`, ` Preferences: ${(cumulative.cumulativeByKind.preference || 0) + stats.byKind.preference}`, ` Project facts: ${(cumulative.cumulativeByKind.project_fact || 0) + stats.byKind.project_fact}`, ` Emotional: ${(cumulative.cumulativeByKind.emotional || 0) + stats.byKind.emotional}`);
45
+ return lines.join('\n');
46
+ },
47
+ });
48
+ // ── memory_scan_toggle ──
49
+ registerTool({
50
+ name: 'memory_scan_toggle',
51
+ description: 'Enable or disable the passive memory scanner. When enabled, the scanner watches conversation turns and auto-saves memory-worthy moments (corrections, preferences, project facts, emotional reactions). Disabling it stops detection but preserves already-saved memories.',
52
+ parameters: {
53
+ action: {
54
+ type: 'string',
55
+ description: '"enable" to start scanning, "disable" to stop, or "status" to check current state',
56
+ required: true,
57
+ },
58
+ },
59
+ tier: 'free',
60
+ timeout: 5_000,
61
+ async execute(args) {
62
+ const action = String(args.action || '').toLowerCase().trim();
63
+ if (action === 'enable' || action === 'on' || action === 'start') {
64
+ if (isScannerEnabled()) {
65
+ return 'Memory scanner is already active.';
66
+ }
67
+ startMemoryScanner();
68
+ return 'Memory scanner enabled. Now passively watching for memory-worthy moments in conversation.';
69
+ }
70
+ if (action === 'disable' || action === 'off' || action === 'stop') {
71
+ if (!isScannerEnabled()) {
72
+ return 'Memory scanner is already paused.';
73
+ }
74
+ const stats = getMemoryScannerStats();
75
+ stopMemoryScanner();
76
+ return `Memory scanner paused. Session summary: ${stats.momentsDetected} moments detected, ${stats.memoriesSaved} memories saved.`;
77
+ }
78
+ if (action === 'status') {
79
+ const active = isScannerEnabled();
80
+ const stats = getMemoryScannerStats();
81
+ return `Scanner is ${active ? 'ACTIVE' : 'PAUSED'}. This session: ${stats.turnsObserved} turns observed, ${stats.memoriesSaved} memories saved.`;
82
+ }
83
+ return 'Invalid action. Use "enable", "disable", or "status".';
84
+ },
85
+ });
86
+ } // end registerMemoryScannerTools
87
+ //# sourceMappingURL=memory-scanner-tools.js.map
@@ -0,0 +1,2 @@
1
+ export declare function registerVoiceInputTools(): void;
2
+ //# sourceMappingURL=voice-input-tools.d.ts.map