clawmate 1.4.2 → 1.5.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/main/ai-bridge.js +12 -0
- package/main/autostart.js +69 -2
- package/main/index.js +13 -2
- package/main/ipc-handlers.js +24 -1
- package/main/platform.js +17 -1
- package/main/proactive-monitor.js +1009 -0
- package/main/tray.js +26 -1
- package/package.json +1 -1
- package/preload/preload.js +7 -0
- package/renderer/index.html +1 -0
- package/renderer/js/app.js +5 -0
- package/renderer/js/browser-watcher.js +6 -0
- package/renderer/js/proactive-controller.js +205 -0
- package/shared/messages.js +408 -0
- package/skills/launch-pet/index.js +86 -4
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proactive Monitor - Watches user activity and triggers pet interventions
|
|
3
|
+
*
|
|
4
|
+
* Detects clipboard changes, active window switches, idle patterns,
|
|
5
|
+
* and complex behavioral patterns to decide when the pet should react.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* ClipboardWatcher (500ms) --+
|
|
9
|
+
* ActiveWindowWatcher (5s) --+--> ContextAnalyzer --> InterventionDecider (cooldown)
|
|
10
|
+
* IdleDetector (10s) --------+ |
|
|
11
|
+
* +-----------+-----------+
|
|
12
|
+
* v v
|
|
13
|
+
* AIBridge.send() IPC 'proactive-event'
|
|
14
|
+
* (when AI connected) |
|
|
15
|
+
* v
|
|
16
|
+
* Renderer ProactiveController
|
|
17
|
+
*/
|
|
18
|
+
const { clipboard, powerMonitor, desktopCapturer, screen } = require('electron');
|
|
19
|
+
const EventEmitter = require('events');
|
|
20
|
+
|
|
21
|
+
// =========================================================================
|
|
22
|
+
// Visual Triggers - 화면 캡처가 AI 판단에 도움이 되는 트리거들
|
|
23
|
+
// =========================================================================
|
|
24
|
+
const VISUAL_TRIGGERS = new Set([
|
|
25
|
+
'error_detected', 'error_loop', 'checkout_detected', 'shopping_detected',
|
|
26
|
+
'coding_detected', 'terminal_active', 'idle_return',
|
|
27
|
+
'video_watching', 'news_reading', 'document_editing', 'learning_activity',
|
|
28
|
+
'finance_activity', 'food_ordering', 'travel_planning', 'search_detected',
|
|
29
|
+
'gaming_detected', 'meeting_detected', 'dev_web_detected', 'reading_pdf',
|
|
30
|
+
'deep_focus', 'social_scrolling', 'wiki_rabbit_hole', 'price_comparison',
|
|
31
|
+
'file_management', 'wiki_browsing', 'email_checking',
|
|
32
|
+
'research_mode', 'procrastination', 'focus_break', 'repeated_search',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// =========================================================================
|
|
36
|
+
// Site/App Category Definitions (for window title matching)
|
|
37
|
+
// =========================================================================
|
|
38
|
+
const SITE_CATEGORIES = {
|
|
39
|
+
shopping: {
|
|
40
|
+
patterns: [
|
|
41
|
+
'amazon', 'ebay', 'coupang', '\uCFE0\uD321', 'gmarket', 'g\uB9C8\uCF13', '11st', '11\uBC88\uAC00',
|
|
42
|
+
'aliexpress', 'shopee', 'etsy', 'auction', '\uC625\uC158', 'tmon', '\uD2F0\uBAAC',
|
|
43
|
+
'wemakeprice', '\uC704\uBA54\uD504', 'musinsa', '\uBB34\uC2E0\uC0AC', 'oliveyoung',
|
|
44
|
+
'\uC62C\uB9AC\uBE0C\uC601', 'walmart', 'target.com', 'bestbuy', 'newegg',
|
|
45
|
+
'rakuten', 'taobao', 'jd.com', 'lazada', 'zalando',
|
|
46
|
+
],
|
|
47
|
+
trigger: 'shopping_detected',
|
|
48
|
+
cooldown: 120000,
|
|
49
|
+
},
|
|
50
|
+
checkout: {
|
|
51
|
+
patterns: [
|
|
52
|
+
'cart', 'checkout', '\uC7A5\uBC14\uAD6C\uB2C8', '\uACB0\uC81C', 'payment',
|
|
53
|
+
'\uC8FC\uBB38', 'order confirm', '\uC8FC\uBB38\uD655\uC778', 'place order',
|
|
54
|
+
'buy now', '\uAD6C\uB9E4\uD558\uAE30', 'proceed to',
|
|
55
|
+
],
|
|
56
|
+
trigger: 'checkout_detected',
|
|
57
|
+
cooldown: 60000,
|
|
58
|
+
},
|
|
59
|
+
news: {
|
|
60
|
+
patterns: [
|
|
61
|
+
'cnn', 'bbc', 'nytimes', 'reuters', 'bloomberg',
|
|
62
|
+
'naver.com/news', '\uB124\uC774\uBC84\uB274\uC2A4', 'daum.net/news', '\uB2E4\uC74C\uB274\uC2A4',
|
|
63
|
+
'hacker news', 'techcrunch', 'the verge', 'ars technica',
|
|
64
|
+
'\uC870\uC120\uC77C\uBCF4', '\uC911\uC559\uC77C\uBCF4', '\uB3D9\uC544\uC77C\uBCF4', '\uD55C\uACBD\uB808',
|
|
65
|
+
],
|
|
66
|
+
trigger: 'news_reading',
|
|
67
|
+
cooldown: 120000,
|
|
68
|
+
},
|
|
69
|
+
social: {
|
|
70
|
+
patterns: [
|
|
71
|
+
'instagram', 'twitter', 'x.com', 'facebook', 'threads',
|
|
72
|
+
'tiktok', 'reddit', 'mastodon', 'bluesky', 'tumblr',
|
|
73
|
+
'linkedin feed', 'pinterest',
|
|
74
|
+
],
|
|
75
|
+
trigger: 'social_scrolling',
|
|
76
|
+
cooldown: 120000,
|
|
77
|
+
},
|
|
78
|
+
video: {
|
|
79
|
+
patterns: [
|
|
80
|
+
'youtube', 'twitch', 'netflix', 'disney+', 'wavve', 'tving',
|
|
81
|
+
'watcha', 'hulu', 'prime video', 'crunchyroll', 'vimeo',
|
|
82
|
+
'dailymotion', 'bilibili', 'niconico',
|
|
83
|
+
],
|
|
84
|
+
trigger: 'video_watching',
|
|
85
|
+
cooldown: 120000,
|
|
86
|
+
},
|
|
87
|
+
coding: {
|
|
88
|
+
patterns: [
|
|
89
|
+
'visual studio code', 'vscode', 'intellij', 'pycharm', 'webstorm',
|
|
90
|
+
'sublime text', 'atom', 'vim ', 'neovim', 'emacs', 'cursor',
|
|
91
|
+
'zed', 'android studio', 'xcode', 'rider', 'goland',
|
|
92
|
+
],
|
|
93
|
+
trigger: 'coding_detected',
|
|
94
|
+
cooldown: 300000,
|
|
95
|
+
},
|
|
96
|
+
terminal: {
|
|
97
|
+
patterns: [
|
|
98
|
+
'powershell', 'cmd.exe', 'command prompt', 'windows terminal',
|
|
99
|
+
'git bash', 'wsl', 'terminal', 'iterm', 'hyper', 'alacritty',
|
|
100
|
+
'warp', 'kitty',
|
|
101
|
+
],
|
|
102
|
+
trigger: 'terminal_active',
|
|
103
|
+
cooldown: 300000,
|
|
104
|
+
},
|
|
105
|
+
music: {
|
|
106
|
+
patterns: [
|
|
107
|
+
'spotify', 'apple music', 'youtube music', 'soundcloud',
|
|
108
|
+
'melon', 'genie', 'bugs', 'flo', 'vibe', 'tidal',
|
|
109
|
+
'deezer', 'pandora', 'amazon music',
|
|
110
|
+
],
|
|
111
|
+
trigger: 'music_playing',
|
|
112
|
+
cooldown: 300000,
|
|
113
|
+
},
|
|
114
|
+
food: {
|
|
115
|
+
patterns: [
|
|
116
|
+
'\uBC30\uB2EC\uC758\uBBFC\uC871', 'baemin', '\uCFE0\uD321\uC774\uCE20', 'coupangeats',
|
|
117
|
+
'\uC694\uAE30\uC694', 'yogiyo', 'ubereats', 'uber eats',
|
|
118
|
+
'doordash', 'grubhub', 'deliveroo', 'just eat',
|
|
119
|
+
],
|
|
120
|
+
trigger: 'food_ordering',
|
|
121
|
+
cooldown: 120000,
|
|
122
|
+
},
|
|
123
|
+
travel: {
|
|
124
|
+
patterns: [
|
|
125
|
+
'booking.com', 'airbnb', 'hotels.com', 'expedia', 'agoda',
|
|
126
|
+
'\uC57C\uB180\uC790', '\uC5EC\uAE30\uC5B4\uB54C', 'trip.com', 'skyscanner',
|
|
127
|
+
'google flights', 'kayak', '\uD2B8\uB9AC\uD50C', 'tripadvisor',
|
|
128
|
+
'\uC778\uD130\uD30C\uD06C', '\uB9C8\uC774\uB9AC\uC5BC\uD2B8\uB9BD',
|
|
129
|
+
],
|
|
130
|
+
trigger: 'travel_planning',
|
|
131
|
+
cooldown: 120000,
|
|
132
|
+
},
|
|
133
|
+
learning: {
|
|
134
|
+
patterns: [
|
|
135
|
+
'udemy', 'coursera', 'khan academy', '\uC778\uD504\uB7F0', 'inflearn',
|
|
136
|
+
'nomadcoders', '\uB178\uB9C8\uB4DC\uCF54\uB354', 'edx', 'skillshare',
|
|
137
|
+
'pluralsight', 'leetcode', 'hackerrank', 'codecademy',
|
|
138
|
+
'duolingo', 'brilliant',
|
|
139
|
+
],
|
|
140
|
+
trigger: 'learning_activity',
|
|
141
|
+
cooldown: 300000,
|
|
142
|
+
},
|
|
143
|
+
email: {
|
|
144
|
+
patterns: [
|
|
145
|
+
'gmail', 'outlook', 'yahoo mail', 'naver mail', '\uB124\uC774\uBC84 \uBA54\uC77C',
|
|
146
|
+
'protonmail', 'zoho mail', 'thunderbird', 'mail -',
|
|
147
|
+
],
|
|
148
|
+
trigger: 'email_checking',
|
|
149
|
+
cooldown: 120000,
|
|
150
|
+
},
|
|
151
|
+
gaming: {
|
|
152
|
+
patterns: [
|
|
153
|
+
'steam', 'epic games', 'league of legends', 'valorant',
|
|
154
|
+
'overwatch', 'minecraft', 'roblox', 'genshin', 'fortnite',
|
|
155
|
+
'apex legends', 'counter-strike', 'dota', 'diablo',
|
|
156
|
+
'lost ark', '\uB85C\uC2A4\uD2B8\uC544\uD06C', 'maplestory', '\uBA54\uC774\uD50C\uC2A4\uD1A0\uB9AC',
|
|
157
|
+
],
|
|
158
|
+
trigger: 'gaming_detected',
|
|
159
|
+
cooldown: 300000,
|
|
160
|
+
},
|
|
161
|
+
login: {
|
|
162
|
+
patterns: [
|
|
163
|
+
'sign in', 'log in', '\uB85C\uADF8\uC778', 'login', 'sign up',
|
|
164
|
+
'\uD68C\uC6D0\uAC00\uC785', 'create account', 'forgot password',
|
|
165
|
+
'\uBE44\uBC00\uBC88\uD638 \uCC3E\uAE30', 'reset password',
|
|
166
|
+
],
|
|
167
|
+
trigger: 'login_page',
|
|
168
|
+
cooldown: 60000,
|
|
169
|
+
},
|
|
170
|
+
finance: {
|
|
171
|
+
patterns: [
|
|
172
|
+
'\uD1A0\uC2A4', 'toss', '\uCE74\uCE74\uC624\uBC45\uD06C', 'kakaobank',
|
|
173
|
+
'\uD0A4\uC6C0\uC99D\uAD8C', '\uBBF8\uB798\uC5D0\uC14B', '\uC0BC\uC131\uC99D\uAD8C',
|
|
174
|
+
'\uC2E0\uD55C\uD22C\uC790', 'robinhood', 'coinbase',
|
|
175
|
+
'binance', 'upbit', '\uC5C5\uBE44\uD2B8', 'trading',
|
|
176
|
+
'\uC8FC\uC2DD', 'stock', '\uC740\uD589', 'bank',
|
|
177
|
+
],
|
|
178
|
+
trigger: 'finance_activity',
|
|
179
|
+
cooldown: 120000,
|
|
180
|
+
},
|
|
181
|
+
document: {
|
|
182
|
+
patterns: [
|
|
183
|
+
'google docs', 'google sheets', 'google slides',
|
|
184
|
+
'notion', 'microsoft word', 'microsoft excel', 'powerpoint',
|
|
185
|
+
'confluence', 'obsidian', 'roam research', 'bear',
|
|
186
|
+
'typora', 'mark text', '\uD55C\uAE00', 'hwp',
|
|
187
|
+
],
|
|
188
|
+
trigger: 'document_editing',
|
|
189
|
+
cooldown: 300000,
|
|
190
|
+
},
|
|
191
|
+
search: {
|
|
192
|
+
patterns: [
|
|
193
|
+
'google.com/search', 'google - ', 'bing.com/search',
|
|
194
|
+
'naver.com/search', '\uB124\uC774\uBC84 \uAC80\uC0C9', 'duckduckgo',
|
|
195
|
+
'search results', '\uAC80\uC0C9\uACB0\uACFC',
|
|
196
|
+
],
|
|
197
|
+
trigger: 'search_detected',
|
|
198
|
+
cooldown: 30000,
|
|
199
|
+
},
|
|
200
|
+
meeting: {
|
|
201
|
+
patterns: [
|
|
202
|
+
'zoom', 'teams', 'google meet', 'webex', 'slack huddle',
|
|
203
|
+
'discord call', 'skype', '\uD654\uC0C1\uD68C\uC758',
|
|
204
|
+
],
|
|
205
|
+
trigger: 'meeting_detected',
|
|
206
|
+
cooldown: 300000,
|
|
207
|
+
},
|
|
208
|
+
wiki: {
|
|
209
|
+
patterns: [
|
|
210
|
+
'wikipedia', '\uC704\uD0A4\uD53C\uB514\uC544', '\uB098\uBB34\uC704\uD0A4', 'namu.wiki',
|
|
211
|
+
'fandom.com', 'wikia',
|
|
212
|
+
],
|
|
213
|
+
trigger: 'wiki_browsing',
|
|
214
|
+
cooldown: 60000,
|
|
215
|
+
},
|
|
216
|
+
dev_web: {
|
|
217
|
+
patterns: [
|
|
218
|
+
'github', 'gitlab', 'bitbucket', 'stackoverflow', 'stack overflow',
|
|
219
|
+
'npm', 'pypi', 'crates.io', 'developer', 'documentation', 'docs',
|
|
220
|
+
'api reference', 'mdn web',
|
|
221
|
+
],
|
|
222
|
+
trigger: 'dev_web_detected',
|
|
223
|
+
cooldown: 120000,
|
|
224
|
+
},
|
|
225
|
+
download: {
|
|
226
|
+
patterns: [
|
|
227
|
+
'download', '\uB2E4\uC6B4\uB85C\uB4DC', 'thanks for downloading',
|
|
228
|
+
'save as', '\uC800\uC7A5',
|
|
229
|
+
],
|
|
230
|
+
trigger: 'download_detected',
|
|
231
|
+
cooldown: 60000,
|
|
232
|
+
},
|
|
233
|
+
reading: {
|
|
234
|
+
patterns: [
|
|
235
|
+
'.pdf', 'adobe reader', 'preview', 'kindle',
|
|
236
|
+
'e-book', 'ebook', 'epub',
|
|
237
|
+
],
|
|
238
|
+
trigger: 'reading_pdf',
|
|
239
|
+
cooldown: 300000,
|
|
240
|
+
},
|
|
241
|
+
file_manager: {
|
|
242
|
+
patterns: [
|
|
243
|
+
'file explorer', '\uD30C\uC77C \uD0D0\uC0C9\uAE30', 'finder', 'nautilus',
|
|
244
|
+
'dolphin', 'thunar', 'files',
|
|
245
|
+
],
|
|
246
|
+
trigger: 'file_management',
|
|
247
|
+
cooldown: 120000,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Error patterns in window titles
|
|
252
|
+
const ERROR_PATTERNS = [
|
|
253
|
+
'error', '404', '500', '503', 'not found', 'crashed',
|
|
254
|
+
'fatal', 'exception', 'fail', 'FAILED', 'denied', 'refused',
|
|
255
|
+
'timed out', 'timeout', '\uC624\uB958', '\uC5D0\uB7EC', '\uC2E4\uD328',
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
// Clipboard content patterns
|
|
259
|
+
const CLIPBOARD_PATTERNS = {
|
|
260
|
+
url: /^https?:\/\//i,
|
|
261
|
+
code: /(?:function\s|const\s|let\s|var\s|import\s|from\s|class\s|def\s|return\s|if\s*\(|for\s*\(|while\s*\(|\{[\s\S]*\}|;$|=>)/m,
|
|
262
|
+
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
263
|
+
phone: /^[\d\s\-+()]{7,20}$/,
|
|
264
|
+
longText: null, // checked by length
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// =========================================================================
|
|
268
|
+
// ProactiveMonitor Class
|
|
269
|
+
// =========================================================================
|
|
270
|
+
class ProactiveMonitor extends EventEmitter {
|
|
271
|
+
constructor() {
|
|
272
|
+
super();
|
|
273
|
+
this.enabled = false;
|
|
274
|
+
this.mainWindow = null;
|
|
275
|
+
this.aiBridge = null;
|
|
276
|
+
|
|
277
|
+
// Watcher intervals
|
|
278
|
+
this._clipboardInterval = null;
|
|
279
|
+
this._windowInterval = null;
|
|
280
|
+
this._idleInterval = null;
|
|
281
|
+
|
|
282
|
+
// Clipboard state
|
|
283
|
+
this._lastClipText = '';
|
|
284
|
+
this._lastClipHasImage = false;
|
|
285
|
+
this._clipHistory = []; // { text, timestamp }
|
|
286
|
+
this._maxClipHistory = 20;
|
|
287
|
+
|
|
288
|
+
// Window state
|
|
289
|
+
this._lastTitle = '';
|
|
290
|
+
this._lastAppName = '';
|
|
291
|
+
this._lastCategory = null;
|
|
292
|
+
this._titleHistory = []; // { title, category, timestamp }
|
|
293
|
+
this._maxTitleHistory = 50;
|
|
294
|
+
this._sameTitleSince = 0; // timestamp of when current title started
|
|
295
|
+
this._sameAppSince = 0; // timestamp of when current app started
|
|
296
|
+
|
|
297
|
+
// Idle state
|
|
298
|
+
this._wasIdle = false;
|
|
299
|
+
this._idleStart = 0;
|
|
300
|
+
|
|
301
|
+
// Cooldown tracking
|
|
302
|
+
this._globalCooldown = 8000;
|
|
303
|
+
this._lastEventTime = 0;
|
|
304
|
+
this._triggerCooldowns = {}; // { triggerType: lastFireTime }
|
|
305
|
+
|
|
306
|
+
// Pattern detection state
|
|
307
|
+
this._appSwitchCount = 0;
|
|
308
|
+
this._appSwitchWindow = []; // timestamps of recent switches
|
|
309
|
+
this._errorCount = 0;
|
|
310
|
+
this._errorWindow = []; // timestamps of recent errors
|
|
311
|
+
this._categoryHistory = []; // recent categories for procrastination detection
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Start monitoring
|
|
316
|
+
*/
|
|
317
|
+
start(mainWindow, aiBridge) {
|
|
318
|
+
this.mainWindow = mainWindow;
|
|
319
|
+
this.aiBridge = aiBridge;
|
|
320
|
+
this.enabled = true;
|
|
321
|
+
|
|
322
|
+
// Initialize clipboard state
|
|
323
|
+
try {
|
|
324
|
+
this._lastClipText = clipboard.readText() || '';
|
|
325
|
+
this._lastClipHasImage = !clipboard.readImage().isEmpty();
|
|
326
|
+
} catch {}
|
|
327
|
+
|
|
328
|
+
this._lastTitle = '';
|
|
329
|
+
this._sameTitleSince = Date.now();
|
|
330
|
+
this._sameAppSince = Date.now();
|
|
331
|
+
|
|
332
|
+
// Start watchers
|
|
333
|
+
this._clipboardInterval = setInterval(() => this._checkClipboard(), 500);
|
|
334
|
+
this._windowInterval = setInterval(() => this._checkActiveWindow(), 5000);
|
|
335
|
+
this._idleInterval = setInterval(() => this._checkIdle(), 10000);
|
|
336
|
+
|
|
337
|
+
// Check time-based triggers every 60s
|
|
338
|
+
this._timeInterval = setInterval(() => this._checkTimeTriggers(), 60000);
|
|
339
|
+
|
|
340
|
+
console.log('[ProactiveMonitor] Started');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Stop monitoring
|
|
345
|
+
*/
|
|
346
|
+
stop() {
|
|
347
|
+
this.enabled = false;
|
|
348
|
+
if (this._clipboardInterval) clearInterval(this._clipboardInterval);
|
|
349
|
+
if (this._windowInterval) clearInterval(this._windowInterval);
|
|
350
|
+
if (this._idleInterval) clearInterval(this._idleInterval);
|
|
351
|
+
if (this._timeInterval) clearInterval(this._timeInterval);
|
|
352
|
+
this._clipboardInterval = null;
|
|
353
|
+
this._windowInterval = null;
|
|
354
|
+
this._idleInterval = null;
|
|
355
|
+
this._timeInterval = null;
|
|
356
|
+
console.log('[ProactiveMonitor] Stopped');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
setEnabled(val) {
|
|
360
|
+
this.enabled = val;
|
|
361
|
+
if (!val) {
|
|
362
|
+
this.stop();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// =========================================================================
|
|
367
|
+
// Clipboard Watcher (500ms)
|
|
368
|
+
// =========================================================================
|
|
369
|
+
_checkClipboard() {
|
|
370
|
+
if (!this.enabled) return;
|
|
371
|
+
const now = Date.now();
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
// Check for screenshot (image in clipboard)
|
|
375
|
+
const img = clipboard.readImage();
|
|
376
|
+
const hasImage = !img.isEmpty();
|
|
377
|
+
|
|
378
|
+
if (hasImage && !this._lastClipHasImage) {
|
|
379
|
+
this._lastClipHasImage = true;
|
|
380
|
+
this._fire('clipboard_screenshot', {
|
|
381
|
+
hasImage: true,
|
|
382
|
+
imageSize: img.getSize(),
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
this._lastClipHasImage = hasImage;
|
|
387
|
+
|
|
388
|
+
// Check for text changes
|
|
389
|
+
const text = clipboard.readText() || '';
|
|
390
|
+
if (text && text !== this._lastClipText) {
|
|
391
|
+
const prevText = this._lastClipText;
|
|
392
|
+
this._lastClipText = text;
|
|
393
|
+
|
|
394
|
+
// Record history
|
|
395
|
+
this._clipHistory.push({ text, timestamp: now });
|
|
396
|
+
if (this._clipHistory.length > this._maxClipHistory) {
|
|
397
|
+
this._clipHistory.shift();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Determine clipboard content type
|
|
401
|
+
const context = { text: text.substring(0, 200), length: text.length };
|
|
402
|
+
|
|
403
|
+
// URL copied
|
|
404
|
+
if (CLIPBOARD_PATTERNS.url.test(text.trim())) {
|
|
405
|
+
this._fire('url_copied', { ...context, url: text.trim().substring(0, 500) });
|
|
406
|
+
}
|
|
407
|
+
// Code copied
|
|
408
|
+
else if (CLIPBOARD_PATTERNS.code.test(text)) {
|
|
409
|
+
this._fire('code_copied', context);
|
|
410
|
+
}
|
|
411
|
+
// Email copied
|
|
412
|
+
else if (CLIPBOARD_PATTERNS.email.test(text.trim())) {
|
|
413
|
+
this._fire('email_copied', { ...context, email: text.trim() });
|
|
414
|
+
}
|
|
415
|
+
// Phone copied
|
|
416
|
+
else if (CLIPBOARD_PATTERNS.phone.test(text.trim())) {
|
|
417
|
+
this._fire('phone_copied', context);
|
|
418
|
+
}
|
|
419
|
+
// Long text copied (500+ chars)
|
|
420
|
+
else if (text.length >= 500) {
|
|
421
|
+
this._fire('long_text_copied', context);
|
|
422
|
+
}
|
|
423
|
+
// General clipboard copy
|
|
424
|
+
else {
|
|
425
|
+
this._fire('clipboard_copy', context);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Check repeated copy (3+ copies in 60s)
|
|
429
|
+
const recentCopies = this._clipHistory.filter(
|
|
430
|
+
(h) => now - h.timestamp < 60000
|
|
431
|
+
);
|
|
432
|
+
if (recentCopies.length >= 3) {
|
|
433
|
+
this._fire('repeated_copy', {
|
|
434
|
+
count: recentCopies.length,
|
|
435
|
+
timespan: 60,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Check search pattern (copy then search engine within 30s)
|
|
440
|
+
this._checkSearchPattern(text, now);
|
|
441
|
+
}
|
|
442
|
+
} catch {
|
|
443
|
+
// Clipboard access can fail silently
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// =========================================================================
|
|
448
|
+
// Active Window Watcher (5s)
|
|
449
|
+
// =========================================================================
|
|
450
|
+
async _checkActiveWindow() {
|
|
451
|
+
if (!this.enabled) return;
|
|
452
|
+
const now = Date.now();
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
const { getActiveWindowTitle } = require('./platform');
|
|
456
|
+
const title = await getActiveWindowTitle();
|
|
457
|
+
if (!title) return;
|
|
458
|
+
|
|
459
|
+
const titleLower = title.toLowerCase();
|
|
460
|
+
const titleChanged = title !== this._lastTitle;
|
|
461
|
+
|
|
462
|
+
if (titleChanged) {
|
|
463
|
+
const prevTitle = this._lastTitle;
|
|
464
|
+
const prevCategory = this._lastCategory;
|
|
465
|
+
this._lastTitle = title;
|
|
466
|
+
|
|
467
|
+
// Detect app name from title (part before " - " or " | ")
|
|
468
|
+
const appName = this._extractAppName(title);
|
|
469
|
+
const appChanged = appName !== this._lastAppName;
|
|
470
|
+
|
|
471
|
+
if (appChanged) {
|
|
472
|
+
const prevApp = this._lastAppName;
|
|
473
|
+
this._lastAppName = appName;
|
|
474
|
+
this._sameAppSince = now;
|
|
475
|
+
|
|
476
|
+
// Fire app_switch
|
|
477
|
+
this._fire('app_switch', {
|
|
478
|
+
from: prevApp,
|
|
479
|
+
to: appName,
|
|
480
|
+
title,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Track app switches for rapid_switching detection
|
|
484
|
+
this._appSwitchWindow.push(now);
|
|
485
|
+
this._appSwitchWindow = this._appSwitchWindow.filter(
|
|
486
|
+
(t) => now - t < 60000
|
|
487
|
+
);
|
|
488
|
+
if (this._appSwitchWindow.length >= 5) {
|
|
489
|
+
this._fire('rapid_switching', {
|
|
490
|
+
count: this._appSwitchWindow.length,
|
|
491
|
+
timespan: 60,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Title changed but same app -> might be tab change
|
|
497
|
+
this._sameTitleSince = now;
|
|
498
|
+
|
|
499
|
+
// Record title history
|
|
500
|
+
const category = this._categorizeTitle(titleLower);
|
|
501
|
+
this._lastCategory = category;
|
|
502
|
+
this._titleHistory.push({ title, category, timestamp: now });
|
|
503
|
+
if (this._titleHistory.length > this._maxTitleHistory) {
|
|
504
|
+
this._titleHistory.shift();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Category history for pattern detection
|
|
508
|
+
if (category) {
|
|
509
|
+
this._categoryHistory.push({ category, timestamp: now });
|
|
510
|
+
if (this._categoryHistory.length > 30) {
|
|
511
|
+
this._categoryHistory.shift();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Fire category-specific triggers
|
|
516
|
+
if (category) {
|
|
517
|
+
const catDef = SITE_CATEGORIES[category];
|
|
518
|
+
if (catDef) {
|
|
519
|
+
this._fire(catDef.trigger, {
|
|
520
|
+
title,
|
|
521
|
+
category,
|
|
522
|
+
appName,
|
|
523
|
+
}, catDef.cooldown);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Error detection
|
|
528
|
+
if (this._isErrorTitle(titleLower)) {
|
|
529
|
+
this._errorWindow.push(now);
|
|
530
|
+
this._errorWindow = this._errorWindow.filter(
|
|
531
|
+
(t) => now - t < 300000
|
|
532
|
+
);
|
|
533
|
+
this._fire('error_detected', { title });
|
|
534
|
+
|
|
535
|
+
// Error loop: 3+ errors in 5 minutes
|
|
536
|
+
if (this._errorWindow.length >= 3) {
|
|
537
|
+
this._fire('error_loop', {
|
|
538
|
+
count: this._errorWindow.length,
|
|
539
|
+
timespan: 300,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Complex pattern detection
|
|
545
|
+
this._checkWikiRabbitHole(titleLower, now);
|
|
546
|
+
this._checkPriceComparison(now);
|
|
547
|
+
this._checkResearchMode(now);
|
|
548
|
+
this._checkProcrastination(now);
|
|
549
|
+
this._checkRepeatedSearch(now);
|
|
550
|
+
} else {
|
|
551
|
+
// Same title - check for long focus
|
|
552
|
+
const focusDuration = now - this._sameTitleSince;
|
|
553
|
+
|
|
554
|
+
// long_focus: same app for 10+ minutes
|
|
555
|
+
if (focusDuration >= 600000) {
|
|
556
|
+
this._fire('long_focus', {
|
|
557
|
+
title: this._lastTitle,
|
|
558
|
+
duration: Math.floor(focusDuration / 1000),
|
|
559
|
+
appName: this._lastAppName,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// deep_focus: same app (IDE/document) for 20+ minutes
|
|
564
|
+
const appDuration = now - this._sameAppSince;
|
|
565
|
+
if (appDuration >= 1200000) {
|
|
566
|
+
const cat = this._lastCategory;
|
|
567
|
+
if (cat === 'coding' || cat === 'document' || cat === 'terminal') {
|
|
568
|
+
this._fire('deep_focus', {
|
|
569
|
+
title: this._lastTitle,
|
|
570
|
+
duration: Math.floor(appDuration / 1000),
|
|
571
|
+
category: cat,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// social_scrolling: same social media for 10+ minutes
|
|
577
|
+
if (this._lastCategory === 'social' && focusDuration >= 600000) {
|
|
578
|
+
this._fire('social_scrolling', {
|
|
579
|
+
title: this._lastTitle,
|
|
580
|
+
duration: Math.floor(focusDuration / 1000),
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
} catch (err) {
|
|
585
|
+
// Window title fetch can fail
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// =========================================================================
|
|
590
|
+
// Idle Detector (10s)
|
|
591
|
+
// =========================================================================
|
|
592
|
+
_checkIdle() {
|
|
593
|
+
if (!this.enabled) return;
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
const idleTime = powerMonitor.getSystemIdleTime(); // seconds
|
|
597
|
+
|
|
598
|
+
if (idleTime > 60 && !this._wasIdle) {
|
|
599
|
+
this._wasIdle = true;
|
|
600
|
+
this._idleStart = Date.now();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (this._wasIdle && idleTime < 5) {
|
|
604
|
+
// User returned from idle
|
|
605
|
+
const idleDuration = Math.floor((Date.now() - this._idleStart) / 1000);
|
|
606
|
+
this._wasIdle = false;
|
|
607
|
+
this._fire('idle_return', {
|
|
608
|
+
idleDuration,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
} catch {}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// =========================================================================
|
|
615
|
+
// Time-based Triggers (60s)
|
|
616
|
+
// =========================================================================
|
|
617
|
+
_checkTimeTriggers() {
|
|
618
|
+
if (!this.enabled) return;
|
|
619
|
+
const now = new Date();
|
|
620
|
+
const hour = now.getHours();
|
|
621
|
+
const day = now.getDay(); // 0=Sun, 6=Sat
|
|
622
|
+
|
|
623
|
+
// late_night: 23:00 ~ 05:00
|
|
624
|
+
if (hour >= 23 || hour < 5) {
|
|
625
|
+
this._fire('late_night', { hour }, 600000);
|
|
626
|
+
|
|
627
|
+
// dawn_coding: 02:00 ~ 05:00 + IDE active
|
|
628
|
+
if (hour >= 2 && hour < 5) {
|
|
629
|
+
const cat = this._lastCategory;
|
|
630
|
+
if (cat === 'coding' || cat === 'terminal') {
|
|
631
|
+
this._fire('dawn_coding', { hour, category: cat }, 600000);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// pre_lunch: 11:30 ~ 12:00
|
|
637
|
+
if (hour === 11 && now.getMinutes() >= 30) {
|
|
638
|
+
this._fire('pre_lunch', { hour }, 1800000); // 30min cooldown
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// end_of_work: 17:30 ~ 18:30
|
|
642
|
+
if ((hour === 17 && now.getMinutes() >= 30) || (hour === 18 && now.getMinutes() <= 30)) {
|
|
643
|
+
this._fire('end_of_work', { hour }, 1800000);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// weekend_work: Sat/Sun + work-related app
|
|
647
|
+
if (day === 0 || day === 6) {
|
|
648
|
+
const cat = this._lastCategory;
|
|
649
|
+
if (cat === 'coding' || cat === 'document' || cat === 'terminal' || cat === 'email') {
|
|
650
|
+
this._fire('weekend_work', { day, category: cat }, 3600000); // 1hr cooldown
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// =========================================================================
|
|
656
|
+
// Complex Pattern Detection
|
|
657
|
+
// =========================================================================
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Wiki rabbit hole: 3+ wiki page changes in 60s
|
|
661
|
+
*/
|
|
662
|
+
_checkWikiRabbitHole(titleLower, now) {
|
|
663
|
+
if (!titleLower.includes('wikipedia') && !titleLower.includes('namu.wiki') &&
|
|
664
|
+
!titleLower.includes('\uB098\uBB34\uC704\uD0A4') && !titleLower.includes('fandom')) return;
|
|
665
|
+
|
|
666
|
+
const recentWiki = this._titleHistory.filter(
|
|
667
|
+
(h) => now - h.timestamp < 60000 && h.category === 'wiki'
|
|
668
|
+
);
|
|
669
|
+
if (recentWiki.length >= 3) {
|
|
670
|
+
this._fire('wiki_rabbit_hole', {
|
|
671
|
+
count: recentWiki.length,
|
|
672
|
+
titles: recentWiki.map((h) => h.title).slice(-3),
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Price comparison: switching between shopping sites 3+ times in 60s
|
|
679
|
+
*/
|
|
680
|
+
_checkPriceComparison(now) {
|
|
681
|
+
const recentShopping = this._titleHistory.filter(
|
|
682
|
+
(h) => now - h.timestamp < 60000 && h.category === 'shopping'
|
|
683
|
+
);
|
|
684
|
+
// Need 3+ distinct titles from shopping category
|
|
685
|
+
const uniqueTitles = new Set(recentShopping.map((h) => h.title));
|
|
686
|
+
if (uniqueTitles.size >= 3) {
|
|
687
|
+
this._fire('price_comparison', {
|
|
688
|
+
count: uniqueTitles.size,
|
|
689
|
+
timespan: 60,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Research mode: search engine + clipboard copies in 30s window
|
|
696
|
+
*/
|
|
697
|
+
_checkResearchMode(now) {
|
|
698
|
+
const recentSearch = this._titleHistory.filter(
|
|
699
|
+
(h) => now - h.timestamp < 30000 && h.category === 'search'
|
|
700
|
+
);
|
|
701
|
+
const recentCopies = this._clipHistory.filter(
|
|
702
|
+
(h) => now - h.timestamp < 30000
|
|
703
|
+
);
|
|
704
|
+
if (recentSearch.length >= 1 && recentCopies.length >= 2) {
|
|
705
|
+
this._fire('research_mode', {
|
|
706
|
+
searches: recentSearch.length,
|
|
707
|
+
copies: recentCopies.length,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Procrastination: rapid switching between work (coding/doc/terminal)
|
|
714
|
+
* and entertainment (social/video/gaming) 3+ times in 60s
|
|
715
|
+
*/
|
|
716
|
+
_checkProcrastination(now) {
|
|
717
|
+
const recent = this._categoryHistory.filter(
|
|
718
|
+
(h) => now - h.timestamp < 60000
|
|
719
|
+
);
|
|
720
|
+
if (recent.length < 4) return;
|
|
721
|
+
|
|
722
|
+
const workCats = new Set(['coding', 'document', 'terminal', 'dev_web']);
|
|
723
|
+
const funCats = new Set(['social', 'video', 'gaming', 'news']);
|
|
724
|
+
|
|
725
|
+
let switches = 0;
|
|
726
|
+
for (let i = 1; i < recent.length; i++) {
|
|
727
|
+
const prev = recent[i - 1].category;
|
|
728
|
+
const curr = recent[i].category;
|
|
729
|
+
if (
|
|
730
|
+
(workCats.has(prev) && funCats.has(curr)) ||
|
|
731
|
+
(funCats.has(prev) && workCats.has(curr))
|
|
732
|
+
) {
|
|
733
|
+
switches++;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (switches >= 3) {
|
|
738
|
+
this._fire('procrastination', {
|
|
739
|
+
switches,
|
|
740
|
+
timespan: 60,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Repeated search: 3+ different search queries in 60s
|
|
747
|
+
* (indicates user can't find what they need)
|
|
748
|
+
*/
|
|
749
|
+
_checkRepeatedSearch(now) {
|
|
750
|
+
const recentSearch = this._titleHistory.filter(
|
|
751
|
+
(h) => now - h.timestamp < 60000 && h.category === 'search'
|
|
752
|
+
);
|
|
753
|
+
const uniqueSearches = new Set(recentSearch.map((h) => h.title));
|
|
754
|
+
if (uniqueSearches.size >= 3) {
|
|
755
|
+
this._fire('repeated_search', {
|
|
756
|
+
count: uniqueSearches.size,
|
|
757
|
+
timespan: 60,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Search pattern: copy text then visit search engine within 30s
|
|
764
|
+
*/
|
|
765
|
+
_checkSearchPattern(copiedText, now) {
|
|
766
|
+
// Set a flag, check when next window title arrives
|
|
767
|
+
this._pendingSearchCheck = { text: copiedText, timestamp: now };
|
|
768
|
+
|
|
769
|
+
// Also check immediately with current title
|
|
770
|
+
if (this._lastCategory === 'search') {
|
|
771
|
+
this._fire('search_pattern', {
|
|
772
|
+
copiedText: copiedText.substring(0, 100),
|
|
773
|
+
searchTitle: this._lastTitle,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// =========================================================================
|
|
779
|
+
// Focus break detection (called when app switches from deep focus)
|
|
780
|
+
// =========================================================================
|
|
781
|
+
_checkFocusBreak(fromCategory, toCategory) {
|
|
782
|
+
const workCats = new Set(['coding', 'document', 'terminal', 'dev_web']);
|
|
783
|
+
const funCats = new Set(['social', 'video', 'gaming']);
|
|
784
|
+
|
|
785
|
+
if (workCats.has(fromCategory) && funCats.has(toCategory)) {
|
|
786
|
+
const focusDuration = Date.now() - this._sameAppSince;
|
|
787
|
+
if (focusDuration >= 1200000) { // was focused 20+ minutes
|
|
788
|
+
this._fire('focus_break', {
|
|
789
|
+
fromCategory,
|
|
790
|
+
toCategory,
|
|
791
|
+
focusDuration: Math.floor(focusDuration / 1000),
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// =========================================================================
|
|
798
|
+
// Helper: Categorize title
|
|
799
|
+
// =========================================================================
|
|
800
|
+
_categorizeTitle(titleLower) {
|
|
801
|
+
for (const [category, def] of Object.entries(SITE_CATEGORIES)) {
|
|
802
|
+
for (const pattern of def.patterns) {
|
|
803
|
+
if (titleLower.includes(pattern.toLowerCase())) {
|
|
804
|
+
return category;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Extract app name from window title
|
|
813
|
+
* "Document.txt - Notepad" -> "Notepad"
|
|
814
|
+
* "Google - Chrome" -> "Chrome"
|
|
815
|
+
*/
|
|
816
|
+
_extractAppName(title) {
|
|
817
|
+
// Common separators: " - ", " | ", " \u2014 "
|
|
818
|
+
const separators = [' - ', ' | ', ' \u2014 ', ' \u2013 '];
|
|
819
|
+
for (const sep of separators) {
|
|
820
|
+
const idx = title.lastIndexOf(sep);
|
|
821
|
+
if (idx > 0) {
|
|
822
|
+
return title.substring(idx + sep.length).trim();
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return title.trim();
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Check if title contains error indicators
|
|
830
|
+
*/
|
|
831
|
+
_isErrorTitle(titleLower) {
|
|
832
|
+
return ERROR_PATTERNS.some((p) => titleLower.includes(p.toLowerCase()));
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// =========================================================================
|
|
836
|
+
// Screen Capture (for visual triggers)
|
|
837
|
+
// =========================================================================
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* 현재 화면을 캡처하여 base64 JPEG로 반환
|
|
841
|
+
* 실패 시 null (graceful degradation)
|
|
842
|
+
*/
|
|
843
|
+
async _captureScreen() {
|
|
844
|
+
try {
|
|
845
|
+
const sources = await desktopCapturer.getSources({
|
|
846
|
+
types: ['screen'],
|
|
847
|
+
thumbnailSize: { width: 960, height: 540 },
|
|
848
|
+
});
|
|
849
|
+
if (sources.length === 0) return null;
|
|
850
|
+
|
|
851
|
+
const thumbnail = sources[0].thumbnail;
|
|
852
|
+
const jpegBuffer = thumbnail.toJPEG(40);
|
|
853
|
+
return {
|
|
854
|
+
image: jpegBuffer.toString('base64'),
|
|
855
|
+
width: thumbnail.getSize().width,
|
|
856
|
+
height: thumbnail.getSize().height,
|
|
857
|
+
};
|
|
858
|
+
} catch {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* 현재 커서 위치 반환
|
|
865
|
+
*/
|
|
866
|
+
_getCursorPosition() {
|
|
867
|
+
try {
|
|
868
|
+
const point = screen.getCursorScreenPoint();
|
|
869
|
+
return { x: point.x, y: point.y };
|
|
870
|
+
} catch {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// =========================================================================
|
|
876
|
+
// Intervention Decider (cooldown + fire)
|
|
877
|
+
// =========================================================================
|
|
878
|
+
async _fire(triggerType, context, customCooldown) {
|
|
879
|
+
if (!this.enabled) return;
|
|
880
|
+
|
|
881
|
+
const now = Date.now();
|
|
882
|
+
|
|
883
|
+
// Global cooldown
|
|
884
|
+
if (now - this._lastEventTime < this._globalCooldown) return;
|
|
885
|
+
|
|
886
|
+
// Per-trigger cooldown
|
|
887
|
+
const cooldown = customCooldown || this._getDefaultCooldown(triggerType);
|
|
888
|
+
const lastFire = this._triggerCooldowns[triggerType] || 0;
|
|
889
|
+
if (now - lastFire < cooldown) return;
|
|
890
|
+
|
|
891
|
+
// Update cooldown tracking
|
|
892
|
+
this._lastEventTime = now;
|
|
893
|
+
this._triggerCooldowns[triggerType] = now;
|
|
894
|
+
|
|
895
|
+
// Build event payload
|
|
896
|
+
const event = {
|
|
897
|
+
trigger: triggerType,
|
|
898
|
+
context: context || {},
|
|
899
|
+
timestamp: now,
|
|
900
|
+
activeTitle: this._lastTitle,
|
|
901
|
+
activeApp: this._lastAppName,
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
// Emit for internal listeners
|
|
905
|
+
this.emit('trigger', event);
|
|
906
|
+
|
|
907
|
+
// Send to renderer via IPC
|
|
908
|
+
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
909
|
+
this.mainWindow.webContents.send('proactive-event', event);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Send to AI Bridge if connected (시각 트리거는 화면 캡처 포함)
|
|
913
|
+
if (this.aiBridge && this.aiBridge.isConnected()) {
|
|
914
|
+
const aiContext = {
|
|
915
|
+
...context,
|
|
916
|
+
activeTitle: this._lastTitle,
|
|
917
|
+
activeApp: this._lastAppName,
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// 시각 트리거: 화면 캡처 + 커서 위치 번들링
|
|
921
|
+
if (VISUAL_TRIGGERS.has(triggerType)) {
|
|
922
|
+
const [screenData, cursor] = await Promise.all([
|
|
923
|
+
this._captureScreen(),
|
|
924
|
+
Promise.resolve(this._getCursorPosition()),
|
|
925
|
+
]);
|
|
926
|
+
if (screenData) {
|
|
927
|
+
aiContext.screen = screenData;
|
|
928
|
+
}
|
|
929
|
+
if (cursor) {
|
|
930
|
+
aiContext.cursor = cursor;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
this.aiBridge.reportProactiveEvent(triggerType, aiContext);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
console.log(`[ProactiveMonitor] Fired: ${triggerType}${VISUAL_TRIGGERS.has(triggerType) ? ' (with screen)' : ''}`);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Default cooldowns per trigger type
|
|
942
|
+
*/
|
|
943
|
+
_getDefaultCooldown(trigger) {
|
|
944
|
+
const cooldowns = {
|
|
945
|
+
// Clipboard triggers
|
|
946
|
+
clipboard_copy: 10000,
|
|
947
|
+
clipboard_screenshot: 30000,
|
|
948
|
+
repeated_copy: 60000,
|
|
949
|
+
url_copied: 15000,
|
|
950
|
+
code_copied: 20000,
|
|
951
|
+
long_text_copied: 30000,
|
|
952
|
+
email_copied: 30000,
|
|
953
|
+
phone_copied: 30000,
|
|
954
|
+
|
|
955
|
+
// App/window triggers
|
|
956
|
+
app_switch: 20000,
|
|
957
|
+
error_detected: 30000,
|
|
958
|
+
error_loop: 120000,
|
|
959
|
+
meeting_detected: 300000,
|
|
960
|
+
rapid_switching: 120000,
|
|
961
|
+
|
|
962
|
+
// Behavior pattern triggers
|
|
963
|
+
search_pattern: 30000,
|
|
964
|
+
idle_return: 60000,
|
|
965
|
+
long_focus: 300000,
|
|
966
|
+
deep_focus: 600000,
|
|
967
|
+
social_scrolling: 300000,
|
|
968
|
+
wiki_rabbit_hole: 120000,
|
|
969
|
+
price_comparison: 120000,
|
|
970
|
+
research_mode: 60000,
|
|
971
|
+
procrastination: 120000,
|
|
972
|
+
focus_break: 120000,
|
|
973
|
+
repeated_search: 60000,
|
|
974
|
+
|
|
975
|
+
// Time triggers
|
|
976
|
+
late_night: 600000,
|
|
977
|
+
dawn_coding: 600000,
|
|
978
|
+
pre_lunch: 1800000,
|
|
979
|
+
end_of_work: 1800000,
|
|
980
|
+
weekend_work: 3600000,
|
|
981
|
+
|
|
982
|
+
// Category triggers (defaults, overridden by SITE_CATEGORIES)
|
|
983
|
+
shopping_detected: 120000,
|
|
984
|
+
checkout_detected: 60000,
|
|
985
|
+
news_reading: 120000,
|
|
986
|
+
video_watching: 120000,
|
|
987
|
+
coding_detected: 300000,
|
|
988
|
+
terminal_active: 300000,
|
|
989
|
+
music_playing: 300000,
|
|
990
|
+
food_ordering: 120000,
|
|
991
|
+
travel_planning: 120000,
|
|
992
|
+
learning_activity: 300000,
|
|
993
|
+
email_checking: 120000,
|
|
994
|
+
gaming_detected: 300000,
|
|
995
|
+
login_page: 60000,
|
|
996
|
+
finance_activity: 120000,
|
|
997
|
+
document_editing: 300000,
|
|
998
|
+
search_detected: 30000,
|
|
999
|
+
wiki_browsing: 60000,
|
|
1000
|
+
dev_web_detected: 120000,
|
|
1001
|
+
download_detected: 60000,
|
|
1002
|
+
reading_pdf: 300000,
|
|
1003
|
+
file_management: 120000,
|
|
1004
|
+
};
|
|
1005
|
+
return cooldowns[trigger] || 30000;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
module.exports = { ProactiveMonitor };
|