@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 +4 -0
- package/dist/cli.js +107 -0
- package/dist/dream.js +13 -0
- package/dist/tools/behavior-tools.d.ts +2 -0
- package/dist/tools/behavior-tools.js +63 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/watchdog.d.ts +32 -0
- package/dist/tools/watchdog.js +356 -0
- package/dist/user-behavior.d.ts +65 -0
- package/dist/user-behavior.js +301 -0
- package/package.json +1 -1
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,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
|
package/dist/tools/index.js
CHANGED
|
@@ -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