@kernel.chat/kbot 3.66.0 → 3.67.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/dist/agent.js CHANGED
@@ -19,6 +19,7 @@ import { getMemoryPrompt, addTurn, getPreviousMessages, getHistory } from './mem
19
19
  import { getDreamPrompt, dreamAfterSession } from './dream.js';
20
20
  import { setBuddyMood } from './buddy.js';
21
21
  import { notifyTurn, startMemoryScanner, stopMemoryScanner } from './memory-scanner.js';
22
+ import { captureUserBehavior } from './user-behavior.js';
22
23
  import { autoCompact, compressToolResult } from './context-manager.js';
23
24
  import { learnedRoute, recordRoute } from './learned-router.js';
24
25
  import { buildCacheablePrompt, createPromptSections } from './prompt-cache.js';
@@ -1007,6 +1008,9 @@ Always quote file paths that contain spaces. Never reference internal system nam
1007
1008
  setBuddyMood('thinking');
1008
1009
  // Start passive memory scanner for this session
1009
1010
  startMemoryScanner();
1011
+ // Capture a behavior snapshot (what apps are open, active window, etc.)
1012
+ // Non-blocking, macOS only, purely local storage
1013
+ captureUserBehavior();
1010
1014
  // ── Gödel limits: detect undecidable loops and hand off to human ──
1011
1015
  const loopDetector = new LoopDetector({
1012
1016
  maxToolRepeats: 5,
package/dist/cli.js CHANGED
@@ -1005,6 +1005,113 @@ async function main() {
1005
1005
  process.stderr.write(formatMachineProfile(profile));
1006
1006
  }
1007
1007
  });
1008
+ // ── Watchdog — Service & System Status Dashboard ──
1009
+ program
1010
+ .command('watchdog')
1011
+ .alias('wd')
1012
+ .description('Service watchdog — live status of all kbot background services and system health')
1013
+ .option('--json', 'Output as JSON')
1014
+ .option('--restart <service>', 'Restart a specific service (email-agent, discovery, serve, discord, mlx, collective-sync, daemon, kbot-local)')
1015
+ .action(async (opts) => {
1016
+ const { getSystemHealth, getServiceStatus, restartService } = await import('./tools/watchdog.js');
1017
+ // ── Restart mode ──
1018
+ if (opts.restart) {
1019
+ const result = restartService(opts.restart);
1020
+ if (result.success) {
1021
+ console.log();
1022
+ console.log(` ${chalk.hex('#4ADE80')('✓')} ${result.message}`);
1023
+ console.log();
1024
+ }
1025
+ else {
1026
+ console.log();
1027
+ console.log(` ${chalk.hex('#F87171')('✗')} ${result.message}`);
1028
+ console.log();
1029
+ }
1030
+ return;
1031
+ }
1032
+ const h = getSystemHealth();
1033
+ // ── JSON mode ──
1034
+ if (opts.json) {
1035
+ console.log(JSON.stringify(h, null, 2));
1036
+ return;
1037
+ }
1038
+ // ── Dashboard rendering ──
1039
+ const ACCENT = chalk.hex('#A78BFA');
1040
+ const GREEN = chalk.hex('#4ADE80');
1041
+ const YELLOW = chalk.hex('#FBBF24');
1042
+ const RED = chalk.hex('#F87171');
1043
+ const DIM = chalk.dim;
1044
+ const running = h.services.filter(s => s.status === 'running').length;
1045
+ const total = h.services.length;
1046
+ const allUp = running === total;
1047
+ // Box drawing
1048
+ const W = 42;
1049
+ const box = {
1050
+ tl: '\u256D', tr: '\u256E', bl: '\u2570', br: '\u256F', h: '\u2500', v: '\u2502',
1051
+ pad: (s, w) => {
1052
+ const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
1053
+ const diff = w - visible.length;
1054
+ return diff > 0 ? s + ' '.repeat(diff) : s;
1055
+ },
1056
+ };
1057
+ const row = (content) => {
1058
+ return ACCENT(box.v) + ' ' + box.pad(content, W - 2) + ' ' + ACCENT(box.v);
1059
+ };
1060
+ console.log();
1061
+ console.log(' ' + ACCENT(`${box.tl}${box.h.repeat(W)}${box.tr}`));
1062
+ console.log(' ' + row(`${ACCENT.bold('\u25C6 KBOT SYSTEM STATUS')}`));
1063
+ console.log(' ' + ACCENT(`${box.tl}${box.h.repeat(W)}${box.tr}`.replace(box.tl, '\u251C').replace(box.tr, '\u2524')));
1064
+ // Service count
1065
+ const svcColor = allUp ? GREEN : running > 0 ? YELLOW : RED;
1066
+ console.log(' ' + row(`Services: ${svcColor(`${running}/${total} running`)}`));
1067
+ // CPU
1068
+ console.log(' ' + row(`CPU Load: ${chalk.white(h.loadAvg)}`));
1069
+ // RAM
1070
+ console.log(' ' + row(`RAM: ${chalk.white(h.memUsed)} / ${DIM(h.memTotal)}`));
1071
+ // Disk
1072
+ console.log(' ' + row(`Disk: ${chalk.white(h.diskFree)} free ${DIM(`/ ${h.diskTotal}`)}`));
1073
+ // Ollama
1074
+ const ollamaColor = h.ollamaStatus === 'online' ? GREEN : RED;
1075
+ const ollamaInfo = h.ollamaModels.length > 0 ? ` ${DIM(`(${h.ollamaModels.length} models)`)}` : '';
1076
+ console.log(' ' + row(`Ollama: ${ollamaColor(h.ollamaStatus)}${ollamaInfo}`));
1077
+ // Dreams
1078
+ console.log(' ' + row(`Dreams: ${ACCENT(`${h.dreamCycles}`)} cycles, ${ACCENT(`${h.dreamInsights}`)} insights`));
1079
+ // Memory
1080
+ console.log(' ' + row(`Memory: ${chalk.white(h.kbotMemorySize)}`));
1081
+ console.log(' ' + ACCENT(`${box.bl}${box.h.repeat(W)}${box.br}`));
1082
+ console.log();
1083
+ // ── Services table ──
1084
+ console.log(` ${chalk.bold('SERVICES')}`);
1085
+ console.log(` ${DIM('\u2500'.repeat(64))}`);
1086
+ for (const s of h.services) {
1087
+ const icon = s.status === 'running' ? GREEN('\u2713')
1088
+ : s.status === 'dead' ? RED('\u2717')
1089
+ : DIM('\u2500');
1090
+ const nameStr = chalk.bold(s.shortName.padEnd(18));
1091
+ const pidStr = s.pid ? DIM(`PID ${String(s.pid).padEnd(8)}`) : DIM('PID -'.padEnd(12));
1092
+ let statusStr;
1093
+ if (s.status === 'running') {
1094
+ statusStr = `CPU ${chalk.white(s.cpu.padEnd(7))} MEM ${chalk.white(s.mem.padEnd(7))} up ${GREEN(s.uptime)}`;
1095
+ }
1096
+ else if (s.status === 'dead') {
1097
+ statusStr = RED('dead — restart with: kbot wd --restart ' + s.shortName);
1098
+ }
1099
+ else {
1100
+ statusStr = DIM('not loaded');
1101
+ }
1102
+ console.log(` ${icon} ${nameStr} ${pidStr} ${statusStr}`);
1103
+ }
1104
+ console.log();
1105
+ // Ollama model list
1106
+ if (h.ollamaModels.length > 0) {
1107
+ console.log(` ${chalk.bold('OLLAMA MODELS')}`);
1108
+ console.log(` ${DIM('\u2500'.repeat(64))}`);
1109
+ for (const m of h.ollamaModels) {
1110
+ console.log(` ${ACCENT('\u25B8')} ${chalk.white(m)}`);
1111
+ }
1112
+ console.log();
1113
+ }
1114
+ });
1008
1115
  program
1009
1116
  .command('hardware')
1010
1117
  .description('Detect your hardware tier and get personalized model recommendations for local AI')
package/dist/dream.js CHANGED
@@ -22,6 +22,7 @@ import { loadMemory } from './memory.js';
22
22
  import { getTopPatterns, getTopSolutions, getProfileSummary, updateProfile, recordPattern, learnFact, } from './learning.js';
23
23
  import { getMemoryScannerStats } from './memory-scanner.js';
24
24
  import { getLearningReport as getMusicLearningReport, getRecentHistory as getMusicRecentHistory, getPreferences as getMusicPreferences, } from './music-learning.js';
25
+ import { getBehaviorForDream } from './user-behavior.js';
25
26
  import { registerAmendment } from './prompt-evolution.js';
26
27
  // ── Constants ──
27
28
  const DREAM_DIR = join(homedir(), '.kbot', 'memory', 'dreams');
@@ -185,6 +186,8 @@ function buildConsolidationPrompt(sessionHistory, existingInsights, existingMemo
185
186
  : '';
186
187
  musicText = report + (recentText ? `\n\n**Recent production events:**\n${recentText}` : '');
187
188
  }
189
+ // ── Tier 7: User computer behavior — desktop observation ──
190
+ const behaviorText = getBehaviorForDream(48) || '(no behavior data yet)';
188
191
  return `You are a memory consolidation system. Analyze this conversation session and ALL accumulated knowledge tiers to extract durable cross-tier insights.
189
192
 
190
193
  EXISTING DREAM INSIGHTS (Tier 4 — Dream Journal):
@@ -208,6 +211,9 @@ ${scannerText}
208
211
  MUSIC PRODUCTION LEARNING (Tier 6 — Musical Memory):
209
212
  ${musicText}
210
213
 
214
+ USER COMPUTER BEHAVIOR (Tier 7 — Desktop Observation):
215
+ ${behaviorText}
216
+
211
217
  SESSION TO CONSOLIDATE:
212
218
  ${historyText}
213
219
 
@@ -226,6 +232,13 @@ For music/production sessions, pay special attention to:
226
232
  - Cross-domain insights (e.g., coding workflow preferences that mirror production habits)
227
233
  Use category "music" for production-specific insights.
228
234
 
235
+ For desktop behavior data, look for:
236
+ - Workflow patterns (which apps are always open together, e.g., IDE + terminal + browser)
237
+ - Productivity habits (active hours, app switching frequency)
238
+ - Context-switching tendencies (many apps vs focused few)
239
+ - Tool preferences (which creative/dev tools dominate)
240
+ - Cross-domain insights (e.g., "user switches to music production apps in evening hours")
241
+
229
242
  Pay special attention to:
230
243
  - Patterns that confirm or contradict existing insights
231
244
  - Scanner corrections that reveal unrecognized preferences
@@ -0,0 +1,2 @@
1
+ export declare function registerBehaviorTools(): void;
2
+ //# sourceMappingURL=behavior-tools.d.ts.map
@@ -0,0 +1,63 @@
1
+ // kbot Behavior Tools — Agent-accessible desktop behavior observation
2
+ //
3
+ // Exposes the user behavior system to kbot's tool system so agents can:
4
+ // - Capture a behavior snapshot (what apps are open now)
5
+ // - View behavior patterns from recent history
6
+ //
7
+ // macOS only. Privacy-conscious — app names and window titles only.
8
+ // No window contents, no screenshots, no keylogging.
9
+ import { registerTool } from './index.js';
10
+ import { captureUserBehavior, getBehaviorSummary } from '../user-behavior.js';
11
+ export function registerBehaviorTools() {
12
+ // ── behavior_snapshot ──
13
+ // Capture current desktop state
14
+ registerTool({
15
+ name: 'behavior_snapshot',
16
+ description: 'Capture a snapshot of the user\'s current desktop state: which apps are visible, which app is active (frontmost), the active window title, screen count, and whether Ollama is running. macOS only. Privacy-conscious — captures app names and window titles only, never window contents.',
17
+ parameters: {},
18
+ tier: 'free',
19
+ timeout: 10_000, // 10s — osascript can be slow
20
+ execute: async () => {
21
+ const snapshot = captureUserBehavior();
22
+ if (!snapshot) {
23
+ return 'Behavior capture unavailable (macOS only, or osascript failed).';
24
+ }
25
+ const lines = [
26
+ 'Behavior Snapshot',
27
+ `Time: ${snapshot.timestamp} (${snapshot.hour}:00, ${['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][snapshot.dayOfWeek]})`,
28
+ `Active app: ${snapshot.activeApp || '(none)'}`,
29
+ `Active window: ${snapshot.activeWindowTitle || '(none)'}`,
30
+ `Screens: ${snapshot.screenCount}`,
31
+ `Ollama running: ${snapshot.ollamaRunning ? 'yes' : 'no'}`,
32
+ '',
33
+ `Visible apps (${snapshot.visibleApps.length}):`,
34
+ ...snapshot.visibleApps.map(a => ` - ${a}`),
35
+ ];
36
+ return lines.join('\n');
37
+ },
38
+ });
39
+ // ── behavior_summary ──
40
+ // Show behavior patterns from recent history
41
+ registerTool({
42
+ name: 'behavior_summary',
43
+ description: 'Show user behavior patterns from recent history: most-used apps, active hours, app switching patterns, app combinations frequently seen together. Reads from stored snapshots. Use this to understand the user\'s workflow habits and desktop patterns.',
44
+ parameters: {
45
+ hours: {
46
+ type: 'number',
47
+ description: 'How many hours of history to analyze (default: 24, max: 168)',
48
+ required: false,
49
+ default: 24,
50
+ },
51
+ },
52
+ tier: 'free',
53
+ execute: async (args) => {
54
+ const hours = Math.min(args.hours || 24, 168);
55
+ const summary = getBehaviorSummary(hours);
56
+ if (!summary) {
57
+ return 'No behavior data available yet. Snapshots are captured at the start of each kbot session. Use behavior_snapshot to capture one now.';
58
+ }
59
+ return summary.text;
60
+ },
61
+ });
62
+ } // end registerBehaviorTools
63
+ //# sourceMappingURL=behavior-tools.js.map
@@ -303,6 +303,8 @@ const LAZY_MODULE_IMPORTS = [
303
303
  { path: './memory-scanner-tools.js', registerFn: 'registerMemoryScannerTools' },
304
304
  { path: './buddy-tools.js', registerFn: 'registerBuddyTools' },
305
305
  { path: './voice-input-tools.js', registerFn: 'registerVoiceInputTools' },
306
+ { path: './watchdog.js', registerFn: 'registerWatchdogTools' },
307
+ { path: './behavior-tools.js', registerFn: 'registerBehaviorTools' },
306
308
  ];
307
309
  /** Track whether lazy tools have been registered */
308
310
  let lazyToolsRegistered = false;
@@ -0,0 +1,32 @@
1
+ export interface ServiceInfo {
2
+ label: string;
3
+ shortName: string;
4
+ pid: number | null;
5
+ status: 'running' | 'dead' | 'not-loaded';
6
+ cpu: string;
7
+ mem: string;
8
+ uptime: string;
9
+ }
10
+ export interface SystemHealth {
11
+ loadAvg: string;
12
+ memFree: string;
13
+ memTotal: string;
14
+ memUsed: string;
15
+ diskFree: string;
16
+ diskUsed: string;
17
+ diskTotal: string;
18
+ ollamaStatus: string;
19
+ ollamaModels: string[];
20
+ kbotMemorySize: string;
21
+ dreamCycles: number;
22
+ dreamInsights: number;
23
+ services: ServiceInfo[];
24
+ }
25
+ export declare function getServiceStatus(): ServiceInfo[];
26
+ export declare function restartService(name: string): {
27
+ success: boolean;
28
+ message: string;
29
+ };
30
+ export declare function getSystemHealth(): SystemHealth;
31
+ export declare function registerWatchdogTools(): void;
32
+ //# sourceMappingURL=watchdog.d.ts.map
@@ -0,0 +1,356 @@
1
+ // kbot Watchdog Tools — Service monitoring and system health dashboard
2
+ //
3
+ // Monitors all kbot background services running via launchd on macOS:
4
+ // - com.kernel.email-agent
5
+ // - com.kernel.kbot-discovery
6
+ // - com.kernel.kbot-serve
7
+ // - com.kernel.discord-bot
8
+ // - com.kernel.mlx-server
9
+ // - com.kernel.kbot-collective-sync
10
+ // - kbot-local MCP
11
+ // - com.kernel.kbot-daemon (15-min cycle)
12
+ //
13
+ // Tools: service_status, service_restart, system_health
14
+ import { registerTool } from './index.js';
15
+ import { execSync } from 'node:child_process';
16
+ import { existsSync, readFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { homedir } from 'node:os';
19
+ // ── Known services ──
20
+ const SERVICE_MAP = {
21
+ 'email-agent': 'com.kernel.email-agent',
22
+ 'discovery': 'com.kernel.kbot-discovery',
23
+ 'serve': 'com.kernel.kbot-serve',
24
+ 'discord': 'com.kernel.discord-bot',
25
+ 'mlx': 'com.kernel.mlx-server',
26
+ 'collective-sync': 'com.kernel.kbot-collective-sync',
27
+ 'daemon': 'com.kernel.kbot-daemon',
28
+ 'kbot-local': 'com.kernel.kbot-local',
29
+ };
30
+ // Reverse map: label -> short name
31
+ const LABEL_TO_SHORT = {};
32
+ for (const [short, label] of Object.entries(SERVICE_MAP)) {
33
+ LABEL_TO_SHORT[label] = short;
34
+ }
35
+ // ── Helpers ──
36
+ function exec(cmd, timeout = 10_000) {
37
+ try {
38
+ return execSync(cmd, { timeout, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
39
+ }
40
+ catch {
41
+ return '';
42
+ }
43
+ }
44
+ function getUid() {
45
+ return exec('id -u');
46
+ }
47
+ function parseServiceLine(line) {
48
+ // launchctl list output: PID\tStatus\tLabel
49
+ const match = line.match(/^(-?\d+|-)?\s+(\d+)\s+(.+)$/);
50
+ if (!match)
51
+ return null;
52
+ const pid = match[1] === '-' || !match[1] ? null : parseInt(match[1], 10);
53
+ const lastExitStatus = parseInt(match[2], 10);
54
+ const label = match[3].trim();
55
+ return { pid, lastExitStatus, label };
56
+ }
57
+ function isPidAlive(pid) {
58
+ try {
59
+ process.kill(pid, 0);
60
+ return true;
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
66
+ function getProcessStats(pid) {
67
+ const raw = exec(`ps -p ${pid} -o %cpu,%mem,etime=`);
68
+ if (!raw)
69
+ return { cpu: '?', mem: '?', uptime: '?' };
70
+ // Output: " 0.1 1.2 01:23:45" or " 0.0 0.3 12:34"
71
+ const parts = raw.trim().split(/\s+/);
72
+ if (parts.length >= 3) {
73
+ return { cpu: `${parts[0]}%`, mem: `${parts[1]}%`, uptime: parts[2] };
74
+ }
75
+ return { cpu: '?', mem: '?', uptime: '?' };
76
+ }
77
+ function formatUptime(etime) {
78
+ // etime can be: "12:34", "01:23:45", "1-01:23:45"
79
+ if (etime === '?')
80
+ return etime;
81
+ const dayMatch = etime.match(/^(\d+)-/);
82
+ if (dayMatch) {
83
+ const days = parseInt(dayMatch[1], 10);
84
+ return `${days}d`;
85
+ }
86
+ const parts = etime.split(':');
87
+ if (parts.length === 3) {
88
+ const h = parseInt(parts[0], 10);
89
+ return h > 0 ? `${h}h` : `${parseInt(parts[1], 10)}m`;
90
+ }
91
+ if (parts.length === 2) {
92
+ const m = parseInt(parts[0], 10);
93
+ return m > 0 ? `${m}m` : `${parseInt(parts[1], 10)}s`;
94
+ }
95
+ return etime;
96
+ }
97
+ // ── Core Functions (exported for CLI use) ──
98
+ export function getServiceStatus() {
99
+ const raw = exec('launchctl list 2>/dev/null');
100
+ if (!raw)
101
+ return [];
102
+ const lines = raw.split('\n');
103
+ const found = new Map();
104
+ for (const line of lines) {
105
+ const parsed = parseServiceLine(line);
106
+ if (!parsed)
107
+ continue;
108
+ // Match against known service labels or grep for kbot/kernel/email
109
+ if (!parsed.label.match(/kbot|kernel|email/i))
110
+ continue;
111
+ const shortName = LABEL_TO_SHORT[parsed.label] || parsed.label.replace(/^com\.kernel\./, '');
112
+ const alive = parsed.pid !== null && parsed.pid > 0 && isPidAlive(parsed.pid);
113
+ const stats = alive && parsed.pid ? getProcessStats(parsed.pid) : { cpu: '-', mem: '-', uptime: '-' };
114
+ found.set(parsed.label, {
115
+ label: parsed.label,
116
+ shortName,
117
+ pid: alive ? parsed.pid : null,
118
+ status: alive ? 'running' : 'dead',
119
+ cpu: stats.cpu,
120
+ mem: stats.mem,
121
+ uptime: formatUptime(stats.uptime),
122
+ });
123
+ }
124
+ // Add known services that weren't found (not loaded)
125
+ for (const [short, label] of Object.entries(SERVICE_MAP)) {
126
+ if (!found.has(label)) {
127
+ found.set(label, {
128
+ label,
129
+ shortName: short,
130
+ pid: null,
131
+ status: 'not-loaded',
132
+ cpu: '-',
133
+ mem: '-',
134
+ uptime: '-',
135
+ });
136
+ }
137
+ }
138
+ // Sort: running first, then dead, then not-loaded
139
+ const order = { running: 0, dead: 1, 'not-loaded': 2 };
140
+ return [...found.values()].sort((a, b) => order[a.status] - order[b.status]);
141
+ }
142
+ export function restartService(name) {
143
+ // Resolve short name to label
144
+ const label = SERVICE_MAP[name] || name;
145
+ const uid = getUid();
146
+ if (!uid) {
147
+ return { success: false, message: 'Could not determine user ID' };
148
+ }
149
+ // Try kickstart first (restarts running services)
150
+ const domain = `gui/${uid}`;
151
+ const result = exec(`launchctl kickstart -k ${domain}/${label} 2>&1`, 15_000);
152
+ if (result.includes('No such process') || result.includes('Could not find service')) {
153
+ // Try bootstrap (load the plist)
154
+ const plistPath = join(homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
155
+ if (existsSync(plistPath)) {
156
+ const loadResult = exec(`launchctl bootstrap ${domain} ${plistPath} 2>&1`, 15_000);
157
+ if (loadResult.includes('error') || loadResult.includes('Could not')) {
158
+ return { success: false, message: `Failed to load ${label}: ${loadResult}` };
159
+ }
160
+ return { success: true, message: `Loaded and started ${label}` };
161
+ }
162
+ return { success: false, message: `Service ${label} not found. Plist missing at ${plistPath}` };
163
+ }
164
+ if (result.includes('error') || result.includes('failed')) {
165
+ return { success: false, message: `Restart failed: ${result}` };
166
+ }
167
+ return { success: true, message: `Restarted ${label} via kickstart` };
168
+ }
169
+ export function getSystemHealth() {
170
+ // ── CPU load ──
171
+ const loadRaw = exec('sysctl -n vm.loadavg');
172
+ // Output: "{ 1.23 1.45 1.67 }"
173
+ const loadAvg = loadRaw.replace(/[{}]/g, '').trim() || '?';
174
+ // ── Memory ──
175
+ let memFree = '?';
176
+ let memTotal = '?';
177
+ let memUsed = '?';
178
+ const vmStatRaw = exec('vm_stat');
179
+ const pageSize = parseInt(exec('sysctl -n hw.pagesize') || '4096', 10);
180
+ const totalMemBytes = parseInt(exec('sysctl -n hw.memsize') || '0', 10);
181
+ if (vmStatRaw && totalMemBytes > 0) {
182
+ const pages = (key) => {
183
+ const m = vmStatRaw.match(new RegExp(`${key}:\\s+(\\d+)`));
184
+ return m ? parseInt(m[1], 10) : 0;
185
+ };
186
+ const freePages = pages('Pages free') + pages('Pages speculative');
187
+ const freeBytes = freePages * pageSize;
188
+ const usedBytes = totalMemBytes - freeBytes;
189
+ memTotal = `${(totalMemBytes / (1024 ** 3)).toFixed(1)}GB`;
190
+ memFree = `${(freeBytes / (1024 ** 3)).toFixed(1)}GB`;
191
+ memUsed = `${(usedBytes / (1024 ** 3)).toFixed(1)}GB`;
192
+ }
193
+ // ── Disk ──
194
+ let diskFree = '?';
195
+ let diskUsed = '?';
196
+ let diskTotal = '?';
197
+ const dfRaw = exec('df -h / 2>/dev/null');
198
+ if (dfRaw) {
199
+ const dfLines = dfRaw.split('\n');
200
+ if (dfLines.length >= 2) {
201
+ const parts = dfLines[1].split(/\s+/);
202
+ // Filesystem Size Used Avail Use% Mounted
203
+ if (parts.length >= 4) {
204
+ diskTotal = parts[1];
205
+ diskUsed = parts[2];
206
+ diskFree = parts[3];
207
+ }
208
+ }
209
+ }
210
+ // ── Ollama ──
211
+ let ollamaStatus = 'offline';
212
+ let ollamaModels = [];
213
+ try {
214
+ const ollamaRaw = exec('curl -s --connect-timeout 2 http://localhost:11434/api/tags');
215
+ if (ollamaRaw) {
216
+ const data = JSON.parse(ollamaRaw);
217
+ if (data.models) {
218
+ ollamaStatus = 'online';
219
+ ollamaModels = data.models.map(m => m.name);
220
+ }
221
+ }
222
+ }
223
+ catch { /* offline */ }
224
+ // ── kbot memory ──
225
+ const kbotMemDir = join(homedir(), '.kbot', 'memory');
226
+ let kbotMemorySize = '0B';
227
+ if (existsSync(kbotMemDir)) {
228
+ const duRaw = exec(`du -sh "${kbotMemDir}" 2>/dev/null`);
229
+ if (duRaw) {
230
+ kbotMemorySize = duRaw.split('\t')[0] || '0B';
231
+ }
232
+ }
233
+ // ── Dream state ──
234
+ let dreamCycles = 0;
235
+ let dreamInsights = 0;
236
+ const dreamStateFile = join(homedir(), '.kbot', 'dream', 'state.json');
237
+ if (existsSync(dreamStateFile)) {
238
+ try {
239
+ const state = JSON.parse(readFileSync(dreamStateFile, 'utf-8'));
240
+ dreamCycles = state.cycles || 0;
241
+ dreamInsights = state.activeInsights || 0;
242
+ }
243
+ catch { /* ignore */ }
244
+ }
245
+ // ── Services ──
246
+ const services = getServiceStatus();
247
+ return {
248
+ loadAvg,
249
+ memFree,
250
+ memTotal,
251
+ memUsed,
252
+ diskFree,
253
+ diskUsed,
254
+ diskTotal,
255
+ ollamaStatus,
256
+ ollamaModels,
257
+ kbotMemorySize,
258
+ dreamCycles,
259
+ dreamInsights,
260
+ services,
261
+ };
262
+ }
263
+ // ── Tool Registration ──
264
+ export function registerWatchdogTools() {
265
+ // ── service_status ──
266
+ registerTool({
267
+ name: 'service_status',
268
+ description: 'List all kbot background services (launchd) with PID, status, CPU%, MEM%, and uptime. Checks email-agent, discovery daemon, kbot serve, discord bot, MLX server, collective sync, kbot-local MCP, and the main daemon.',
269
+ parameters: {},
270
+ tier: 'free',
271
+ timeout: 15_000,
272
+ execute: async () => {
273
+ const services = getServiceStatus();
274
+ if (services.length === 0) {
275
+ return 'No kbot services found in launchd. Run `kbot daemon start` to enable background services.';
276
+ }
277
+ const running = services.filter(s => s.status === 'running').length;
278
+ const total = services.length;
279
+ const lines = [
280
+ `KBOT SERVICES: ${running}/${total} running`,
281
+ '',
282
+ 'SERVICE PID STATUS CPU MEM UPTIME',
283
+ '─'.repeat(72),
284
+ ];
285
+ for (const s of services) {
286
+ const icon = s.status === 'running' ? '[OK]' : s.status === 'dead' ? '[!!]' : '[--]';
287
+ const pid = s.pid ? String(s.pid) : '-';
288
+ lines.push(`${icon} ${s.shortName.padEnd(18)} ${pid.padEnd(8)} ${s.status.padEnd(12)} ${s.cpu.padEnd(7)} ${s.mem.padEnd(7)} ${s.uptime}`);
289
+ }
290
+ return lines.join('\n');
291
+ },
292
+ });
293
+ // ── service_restart ──
294
+ registerTool({
295
+ name: 'service_restart',
296
+ description: 'Restart a kbot background service by name. Use short names: email-agent, discovery, serve, discord, mlx, collective-sync, daemon, kbot-local. Runs launchctl kickstart to restart the service.',
297
+ parameters: {
298
+ service: {
299
+ type: 'string',
300
+ description: 'Service short name: email-agent, discovery, serve, discord, mlx, collective-sync, daemon, kbot-local',
301
+ required: true,
302
+ },
303
+ },
304
+ tier: 'free',
305
+ timeout: 20_000,
306
+ execute: async (args) => {
307
+ const name = args.service;
308
+ if (!name)
309
+ return 'Error: service name required';
310
+ // Validate
311
+ const known = Object.keys(SERVICE_MAP);
312
+ if (!SERVICE_MAP[name] && !name.startsWith('com.kernel.')) {
313
+ return `Unknown service "${name}". Known services: ${known.join(', ')}`;
314
+ }
315
+ const result = restartService(name);
316
+ return result.success
317
+ ? `OK: ${result.message}`
318
+ : `FAIL: ${result.message}`;
319
+ },
320
+ });
321
+ // ── system_health ──
322
+ registerTool({
323
+ name: 'system_health',
324
+ description: 'Full system health dashboard: CPU load, RAM, disk, Ollama models, kbot memory size, dream journal status, and all service statuses. Compact overview of everything running on this machine.',
325
+ parameters: {},
326
+ tier: 'free',
327
+ timeout: 20_000,
328
+ execute: async () => {
329
+ const h = getSystemHealth();
330
+ const running = h.services.filter(s => s.status === 'running').length;
331
+ const total = h.services.length;
332
+ const lines = [
333
+ 'KBOT SYSTEM HEALTH',
334
+ '═══════════════════',
335
+ '',
336
+ `Services: ${running}/${total} running`,
337
+ `CPU Load: ${h.loadAvg}`,
338
+ `RAM: ${h.memUsed} used / ${h.memTotal} total (${h.memFree} free)`,
339
+ `Disk: ${h.diskUsed} used / ${h.diskTotal} total (${h.diskFree} free)`,
340
+ `Ollama: ${h.ollamaStatus}${h.ollamaModels.length > 0 ? ` — ${h.ollamaModels.length} models: ${h.ollamaModels.join(', ')}` : ''}`,
341
+ `Memory: ${h.kbotMemorySize}`,
342
+ `Dreams: ${h.dreamCycles} cycles, ${h.dreamInsights} active insights`,
343
+ '',
344
+ 'SERVICES',
345
+ '─'.repeat(72),
346
+ ];
347
+ for (const s of h.services) {
348
+ const icon = s.status === 'running' ? '[OK]' : s.status === 'dead' ? '[!!]' : '[--]';
349
+ const pid = s.pid ? String(s.pid) : '-';
350
+ lines.push(`${icon} ${s.shortName.padEnd(18)} PID ${pid.padEnd(8)} CPU ${s.cpu.padEnd(7)} MEM ${s.mem.padEnd(7)} up ${s.uptime}`);
351
+ }
352
+ return lines.join('\n');
353
+ },
354
+ });
355
+ }
356
+ //# sourceMappingURL=watchdog.js.map
@@ -0,0 +1,65 @@
1
+ export interface BehaviorSnapshot {
2
+ /** ISO timestamp */
3
+ timestamp: string;
4
+ /** Hour of day (0-23) */
5
+ hour: number;
6
+ /** Day of week (0=Sunday, 6=Saturday) */
7
+ dayOfWeek: number;
8
+ /** Visible app names */
9
+ visibleApps: string[];
10
+ /** Active (frontmost) app name */
11
+ activeApp: string | null;
12
+ /** Active window title */
13
+ activeWindowTitle: string | null;
14
+ /** Number of connected screens */
15
+ screenCount: number;
16
+ /** Whether Ollama daemon is running */
17
+ ollamaRunning: boolean;
18
+ }
19
+ export interface BehaviorSummary {
20
+ /** Number of snapshots analyzed */
21
+ snapshotCount: number;
22
+ /** Hours covered */
23
+ hoursCovered: number;
24
+ /** Apps ranked by frequency (name → count) */
25
+ topApps: Array<{
26
+ app: string;
27
+ count: number;
28
+ percent: number;
29
+ }>;
30
+ /** Active hours (hour → snapshot count) */
31
+ activeHours: Record<number, number>;
32
+ /** App combinations seen together (sorted by frequency) */
33
+ appCombinations: Array<{
34
+ apps: string[];
35
+ count: number;
36
+ }>;
37
+ /** Average number of visible apps per snapshot */
38
+ avgVisibleApps: number;
39
+ /** Most common active app */
40
+ mostActiveApp: string | null;
41
+ /** Ollama usage rate (0-1) */
42
+ ollamaUsageRate: number;
43
+ /** Human-readable summary */
44
+ text: string;
45
+ }
46
+ /**
47
+ * Capture a behavior snapshot right now.
48
+ * Runs osascript to detect visible apps, active window, screen count, etc.
49
+ * Stores the snapshot to ~/.kbot/memory/behavior/ as a timestamped JSON file.
50
+ *
51
+ * Non-blocking-safe: catches all errors so it never crashes the caller.
52
+ * macOS only — returns null on other platforms.
53
+ */
54
+ export declare function captureUserBehavior(): BehaviorSnapshot | null;
55
+ /**
56
+ * Read recent snapshots and produce a behavior summary.
57
+ * @param hours How many hours of history to analyze (default: 24)
58
+ */
59
+ export declare function getBehaviorSummary(hours?: number): BehaviorSummary | null;
60
+ /**
61
+ * Get a compact text summary suitable for dream engine injection.
62
+ * Returns null if no data available.
63
+ */
64
+ export declare function getBehaviorForDream(hours?: number): string | null;
65
+ //# sourceMappingURL=user-behavior.d.ts.map
@@ -0,0 +1,301 @@
1
+ // kbot User Behavior — Passive Desktop Observation
2
+ //
3
+ // Captures what the user is doing on their machine: which apps are open,
4
+ // active window, time of day, screen count, whether Ollama is running.
5
+ // Snapshots are stored locally at ~/.kbot/memory/behavior/ as timestamped JSON.
6
+ //
7
+ // Privacy-conscious: captures app names and window titles only.
8
+ // No window contents, no screenshots, no keylogging. macOS only (osascript).
9
+ //
10
+ // Used by the dream engine (Tier 7) to consolidate workflow patterns:
11
+ // - Most-used apps, active hours, app switching habits
12
+ // - App combinations (e.g., "Ableton + Chrome often open together")
13
+ // - Productivity rhythm, context-switching tendencies
14
+ import { execSync } from 'node:child_process';
15
+ import { homedir, platform } from 'node:os';
16
+ import { join } from 'node:path';
17
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync, } from 'node:fs';
18
+ // ── Constants ──
19
+ const BEHAVIOR_DIR = join(homedir(), '.kbot', 'memory', 'behavior');
20
+ const MAX_SNAPSHOTS = 100;
21
+ // ── Helpers ──
22
+ function ensureDir() {
23
+ if (!existsSync(BEHAVIOR_DIR))
24
+ mkdirSync(BEHAVIOR_DIR, { recursive: true });
25
+ }
26
+ function exec(cmd, timeoutMs = 3000) {
27
+ try {
28
+ return execSync(cmd, {
29
+ encoding: 'utf-8',
30
+ timeout: timeoutMs,
31
+ stdio: ['pipe', 'pipe', 'pipe'],
32
+ }).trim();
33
+ }
34
+ catch {
35
+ return '';
36
+ }
37
+ }
38
+ /** Get list of visible app names via AppleScript (macOS only) */
39
+ function getVisibleApps() {
40
+ if (platform() !== 'darwin')
41
+ return [];
42
+ const raw = exec('osascript -e \'tell application "System Events" to get name of every process whose visible is true\'', 5000);
43
+ if (!raw)
44
+ return [];
45
+ // AppleScript returns comma-separated list
46
+ return raw.split(',').map(s => s.trim()).filter(Boolean);
47
+ }
48
+ /** Get the frontmost app name */
49
+ function getActiveApp() {
50
+ if (platform() !== 'darwin')
51
+ return null;
52
+ const raw = exec('osascript -e \'tell application "System Events" to get name of first process whose frontmost is true\'');
53
+ return raw || null;
54
+ }
55
+ /** Get the active window title */
56
+ function getActiveWindowTitle() {
57
+ if (platform() !== 'darwin')
58
+ return null;
59
+ const raw = exec('osascript -e \'tell application "System Events" to get title of front window of (first process whose frontmost is true)\'');
60
+ // osascript returns empty or error if no window
61
+ if (!raw || raw.includes('error') || raw.includes('missing value'))
62
+ return null;
63
+ return raw;
64
+ }
65
+ /** Get the number of connected screens */
66
+ function getScreenCount() {
67
+ if (platform() !== 'darwin')
68
+ return 1;
69
+ const raw = exec('system_profiler SPDisplaysDataType 2>/dev/null', 5000);
70
+ if (!raw)
71
+ return 1;
72
+ const matches = raw.match(/Resolution:/g);
73
+ return matches ? matches.length : 1;
74
+ }
75
+ /** Check if Ollama process is running */
76
+ function isOllamaRunning() {
77
+ const raw = exec('pgrep -x ollama 2>/dev/null || pgrep -f "ollama serve" 2>/dev/null');
78
+ return raw.length > 0;
79
+ }
80
+ // ── Snapshot Management ──
81
+ function getSnapshotFiles() {
82
+ ensureDir();
83
+ return readdirSync(BEHAVIOR_DIR)
84
+ .filter(f => f.endsWith('.json') && f.startsWith('snap_'))
85
+ .sort();
86
+ }
87
+ /** Prune old snapshots beyond MAX_SNAPSHOTS (delete oldest) */
88
+ function pruneSnapshots() {
89
+ const files = getSnapshotFiles();
90
+ if (files.length <= MAX_SNAPSHOTS)
91
+ return;
92
+ const toDelete = files.slice(0, files.length - MAX_SNAPSHOTS);
93
+ for (const f of toDelete) {
94
+ try {
95
+ unlinkSync(join(BEHAVIOR_DIR, f));
96
+ }
97
+ catch { /* ignore */ }
98
+ }
99
+ }
100
+ function loadSnapshot(filename) {
101
+ try {
102
+ const raw = readFileSync(join(BEHAVIOR_DIR, filename), 'utf-8');
103
+ return JSON.parse(raw);
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ // ── Public API ──
110
+ /**
111
+ * Capture a behavior snapshot right now.
112
+ * Runs osascript to detect visible apps, active window, screen count, etc.
113
+ * Stores the snapshot to ~/.kbot/memory/behavior/ as a timestamped JSON file.
114
+ *
115
+ * Non-blocking-safe: catches all errors so it never crashes the caller.
116
+ * macOS only — returns null on other platforms.
117
+ */
118
+ export function captureUserBehavior() {
119
+ if (platform() !== 'darwin')
120
+ return null;
121
+ try {
122
+ const now = new Date();
123
+ const snapshot = {
124
+ timestamp: now.toISOString(),
125
+ hour: now.getHours(),
126
+ dayOfWeek: now.getDay(),
127
+ visibleApps: getVisibleApps(),
128
+ activeApp: getActiveApp(),
129
+ activeWindowTitle: getActiveWindowTitle(),
130
+ screenCount: getScreenCount(),
131
+ ollamaRunning: isOllamaRunning(),
132
+ };
133
+ ensureDir();
134
+ const filename = `snap_${now.toISOString().replace(/[:.]/g, '-')}.json`;
135
+ writeFileSync(join(BEHAVIOR_DIR, filename), JSON.stringify(snapshot, null, 2));
136
+ // Prune old snapshots
137
+ pruneSnapshots();
138
+ return snapshot;
139
+ }
140
+ catch {
141
+ // Non-critical — never crash the caller
142
+ return null;
143
+ }
144
+ }
145
+ /**
146
+ * Read recent snapshots and produce a behavior summary.
147
+ * @param hours How many hours of history to analyze (default: 24)
148
+ */
149
+ export function getBehaviorSummary(hours = 24) {
150
+ try {
151
+ const files = getSnapshotFiles();
152
+ if (files.length === 0)
153
+ return null;
154
+ const cutoff = Date.now() - hours * 60 * 60 * 1000;
155
+ const snapshots = [];
156
+ // Read from newest to oldest, stop when out of range
157
+ for (let i = files.length - 1; i >= 0; i--) {
158
+ const snap = loadSnapshot(files[i]);
159
+ if (!snap)
160
+ continue;
161
+ if (new Date(snap.timestamp).getTime() < cutoff)
162
+ break;
163
+ snapshots.push(snap);
164
+ }
165
+ if (snapshots.length === 0)
166
+ return null;
167
+ // ── App frequency ──
168
+ const appCounts = new Map();
169
+ for (const snap of snapshots) {
170
+ for (const app of snap.visibleApps) {
171
+ appCounts.set(app, (appCounts.get(app) || 0) + 1);
172
+ }
173
+ }
174
+ const topApps = Array.from(appCounts.entries())
175
+ .sort((a, b) => b[1] - a[1])
176
+ .slice(0, 15)
177
+ .map(([app, count]) => ({
178
+ app,
179
+ count,
180
+ percent: Math.round((count / snapshots.length) * 100),
181
+ }));
182
+ // ── Active hours ──
183
+ const activeHours = {};
184
+ for (const snap of snapshots) {
185
+ activeHours[snap.hour] = (activeHours[snap.hour] || 0) + 1;
186
+ }
187
+ // ── App combinations (pairs seen together) ──
188
+ const pairCounts = new Map();
189
+ for (const snap of snapshots) {
190
+ const apps = snap.visibleApps.slice().sort();
191
+ for (let i = 0; i < apps.length; i++) {
192
+ for (let j = i + 1; j < apps.length; j++) {
193
+ const key = `${apps[i]}|${apps[j]}`;
194
+ pairCounts.set(key, (pairCounts.get(key) || 0) + 1);
195
+ }
196
+ }
197
+ }
198
+ const appCombinations = Array.from(pairCounts.entries())
199
+ .filter(([, count]) => count >= 2) // Only show pairs seen 2+ times
200
+ .sort((a, b) => b[1] - a[1])
201
+ .slice(0, 10)
202
+ .map(([key, count]) => ({
203
+ apps: key.split('|'),
204
+ count,
205
+ }));
206
+ // ── Average visible apps ──
207
+ const totalVisible = snapshots.reduce((sum, s) => sum + s.visibleApps.length, 0);
208
+ const avgVisibleApps = Math.round((totalVisible / snapshots.length) * 10) / 10;
209
+ // ── Most common active app ──
210
+ const activeCounts = new Map();
211
+ for (const snap of snapshots) {
212
+ if (snap.activeApp) {
213
+ activeCounts.set(snap.activeApp, (activeCounts.get(snap.activeApp) || 0) + 1);
214
+ }
215
+ }
216
+ const mostActiveApp = activeCounts.size > 0
217
+ ? Array.from(activeCounts.entries()).sort((a, b) => b[1] - a[1])[0][0]
218
+ : null;
219
+ // ── Ollama usage ──
220
+ const ollamaCount = snapshots.filter(s => s.ollamaRunning).length;
221
+ const ollamaUsageRate = Math.round((ollamaCount / snapshots.length) * 100) / 100;
222
+ // ── Human-readable text ──
223
+ const textLines = [];
224
+ textLines.push(`Behavior summary (${snapshots.length} snapshots over ${hours}h):`);
225
+ if (topApps.length > 0) {
226
+ textLines.push(`\nTop apps: ${topApps.slice(0, 8).map(a => `${a.app} (${a.percent}%)`).join(', ')}`);
227
+ }
228
+ if (mostActiveApp) {
229
+ textLines.push(`Most focused app: ${mostActiveApp}`);
230
+ }
231
+ // Active hours summary
232
+ const hourEntries = Object.entries(activeHours)
233
+ .map(([h, c]) => ({ hour: parseInt(h), count: c }))
234
+ .sort((a, b) => b.count - a.count);
235
+ if (hourEntries.length > 0) {
236
+ const peakHours = hourEntries.slice(0, 3).map(e => `${e.hour}:00`).join(', ');
237
+ textLines.push(`Peak activity hours: ${peakHours}`);
238
+ }
239
+ if (appCombinations.length > 0) {
240
+ textLines.push(`\nCommon app pairings:`);
241
+ for (const combo of appCombinations.slice(0, 5)) {
242
+ textLines.push(` ${combo.apps.join(' + ')} (${combo.count}x)`);
243
+ }
244
+ }
245
+ textLines.push(`\nAvg visible apps: ${avgVisibleApps}`);
246
+ textLines.push(`Ollama running: ${Math.round(ollamaUsageRate * 100)}% of the time`);
247
+ return {
248
+ snapshotCount: snapshots.length,
249
+ hoursCovered: hours,
250
+ topApps,
251
+ activeHours,
252
+ appCombinations,
253
+ avgVisibleApps,
254
+ mostActiveApp,
255
+ ollamaUsageRate,
256
+ text: textLines.join('\n'),
257
+ };
258
+ }
259
+ catch {
260
+ return null;
261
+ }
262
+ }
263
+ /**
264
+ * Get a compact text summary suitable for dream engine injection.
265
+ * Returns null if no data available.
266
+ */
267
+ export function getBehaviorForDream(hours = 48) {
268
+ const summary = getBehaviorSummary(hours);
269
+ if (!summary || summary.snapshotCount < 2)
270
+ return null;
271
+ const lines = [];
272
+ // Top apps
273
+ if (summary.topApps.length > 0) {
274
+ lines.push(`Top apps by frequency: ${summary.topApps.slice(0, 10).map(a => `${a.app} (${a.percent}%)`).join(', ')}`);
275
+ }
276
+ // Most focused
277
+ if (summary.mostActiveApp) {
278
+ lines.push(`Most focused (frontmost) app: ${summary.mostActiveApp}`);
279
+ }
280
+ // Active hours
281
+ const hourEntries = Object.entries(summary.activeHours)
282
+ .map(([h, c]) => ({ hour: parseInt(h), count: c }))
283
+ .sort((a, b) => b.count - a.count);
284
+ if (hourEntries.length > 0) {
285
+ const peakHours = hourEntries.slice(0, 4).map(e => `${e.hour}:00`).join(', ');
286
+ lines.push(`Peak activity hours: ${peakHours}`);
287
+ }
288
+ // App combinations
289
+ if (summary.appCombinations.length > 0) {
290
+ const combos = summary.appCombinations.slice(0, 5)
291
+ .map(c => `${c.apps.join(' + ')} (${c.count}x)`)
292
+ .join('; ');
293
+ lines.push(`App combinations: ${combos}`);
294
+ }
295
+ // Context
296
+ lines.push(`Avg visible apps: ${summary.avgVisibleApps}`);
297
+ lines.push(`Ollama running: ${Math.round(summary.ollamaUsageRate * 100)}% of snapshots`);
298
+ lines.push(`Based on ${summary.snapshotCount} snapshots over ${summary.hoursCovered}h`);
299
+ return lines.join('\n');
300
+ }
301
+ //# sourceMappingURL=user-behavior.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernel.chat/kbot",
3
- "version": "3.66.0",
3
+ "version": "3.67.0",
4
4
  "description": "Open-source terminal AI agent. 686+ tools, 35 agents, 20 providers. Fully local, fully sovereign. MIT.",
5
5
  "type": "module",
6
6
  "repository": {