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/tray.js CHANGED
@@ -104,12 +104,13 @@ function createClawIcon() {
104
104
  return nativeImage.createFromBuffer(buffer, { width: size, height: size });
105
105
  }
106
106
 
107
- function setupTray(mainWindow, bridge) {
107
+ function setupTray(mainWindow, bridge, getProactiveMonitor) {
108
108
  aiBridge = bridge;
109
109
  const store = new Store('clawmate-config', {
110
110
  mode: 'pet',
111
111
  character: 'default',
112
112
  telegramToken: '',
113
+ proactiveEnabled: true,
113
114
  });
114
115
 
115
116
  const icon = createClawIcon();
@@ -123,6 +124,7 @@ function setupTray(mainWindow, bridge) {
123
124
  const autoStart = isAutoStartEnabled();
124
125
  const currentChar = store.get('character') || 'default';
125
126
  const hasTelegramToken = !!(store.get('telegramToken'));
127
+ const proactiveEnabled = store.get('proactiveEnabled') !== false;
126
128
 
127
129
  // Character submenu
128
130
  const characterSubmenu = Object.entries(CHARACTER_PRESETS).map(([key, preset]) => ({
@@ -209,6 +211,29 @@ function setupTray(mainWindow, bridge) {
209
211
  { type: 'separator' },
210
212
 
211
213
  // === Settings ===
214
+ {
215
+ label: 'Proactive Mode',
216
+ sublabel: 'Pet reacts to your activity',
217
+ type: 'checkbox',
218
+ checked: proactiveEnabled,
219
+ click: (item) => {
220
+ store.set('proactiveEnabled', item.checked);
221
+ const monitor = getProactiveMonitor ? getProactiveMonitor() : null;
222
+ if (monitor) {
223
+ if (item.checked) {
224
+ if (!monitor.enabled) monitor.start(mainWindow, aiBridge);
225
+ } else {
226
+ monitor.stop();
227
+ }
228
+ }
229
+ if (mainWindow && !mainWindow.isDestroyed()) {
230
+ mainWindow.webContents.send('ai-command', {
231
+ type: 'speak',
232
+ payload: { text: item.checked ? 'Proactive mode ON! I\'ll watch what you do~' : 'Proactive mode off. I\'ll mind my own business.' },
233
+ });
234
+ }
235
+ },
236
+ },
212
237
  {
213
238
  label: 'File Interaction',
214
239
  type: 'checkbox',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmate",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "ClawMate - Give your AI a living body on screen",
5
5
  "main": "main/index.js",
6
6
  "bin": {
@@ -78,4 +78,11 @@ contextBridge.exposeInMainWorld('clawmate', {
78
78
  smartFileOp: (command) => ipcRenderer.invoke('smart-file-op', command),
79
79
  undoSmartMove: (moveId) => ipcRenderer.invoke('undo-smart-move', moveId),
80
80
  undoAllSmartMoves: () => ipcRenderer.invoke('undo-all-smart-moves'),
81
+
82
+ // === Proactive Monitor ===
83
+ onProactiveEvent: (callback) => {
84
+ ipcRenderer.on('proactive-event', (_, event) => callback(event));
85
+ },
86
+ getProactiveConfig: () => ipcRenderer.invoke('get-proactive-config'),
87
+ setProactiveEnabled: (enabled) => ipcRenderer.invoke('set-proactive-enabled', enabled),
81
88
  });
@@ -63,6 +63,7 @@
63
63
  <script src="js/metrics.js"></script>
64
64
  <script src="js/browser-watcher.js"></script>
65
65
  <script src="js/ai-controller.js"></script>
66
+ <script src="js/proactive-controller.js"></script>
66
67
  <script src="js/app.js"></script>
67
68
  </body>
68
69
  </html>
@@ -84,6 +84,11 @@
84
84
  BrowserWatcher.init();
85
85
  }
86
86
 
87
+ // Initialize proactive controller (activity-aware reactions)
88
+ if (typeof ProactiveController !== 'undefined') {
89
+ ProactiveController.init();
90
+ }
91
+
87
92
  // Start engine
88
93
  PetEngine.start();
89
94
 
@@ -37,6 +37,12 @@ const BrowserWatcher = (() => {
37
37
  // Don't comment when in sleeping state
38
38
  if (typeof StateMachine !== 'undefined' && StateMachine.getState() === 'sleeping') return;
39
39
 
40
+ // Skip if ProactiveController reacted within last 5 seconds (avoid duplicate reactions)
41
+ if (typeof ProactiveController !== 'undefined') {
42
+ const lastProactive = ProactiveController.getLastReactionTime();
43
+ if (Date.now() - lastProactive < 5000) return;
44
+ }
45
+
40
46
  try {
41
47
  const title = await window.clawmate.getActiveWindowTitle();
42
48
  if (!title) return;
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Proactive Controller (Renderer)
3
+ *
4
+ * Receives proactive-event from main process ProactiveMonitor
5
+ * and makes the pet react appropriately.
6
+ *
7
+ * Two modes:
8
+ * AI connected: Forward context to AI -> AI decides pet reaction
9
+ * Autonomous: Use preset messages with probabilistic selection + emotion mapping
10
+ */
11
+ const ProactiveController = (() => {
12
+ const REACTION_CHANCE = 0.6; // 60% chance of reacting to a trigger
13
+ const HIGH_PRIORITY_CHANCE = 0.9; // 90% for high priority triggers
14
+ let lastReactionTime = 0;
15
+ let enabled = true;
16
+
17
+ // High priority triggers that should almost always get a reaction
18
+ const HIGH_PRIORITY = new Set([
19
+ 'idle_return', 'error_detected', 'error_loop', 'checkout_detected',
20
+ 'clipboard_screenshot', 'late_night', 'dawn_coding',
21
+ ]);
22
+
23
+ // Low priority triggers that have a lower chance of reaction
24
+ const LOW_PRIORITY = new Set([
25
+ 'app_switch', 'clipboard_copy', 'search_detected', 'login_page',
26
+ 'email_copied', 'phone_copied',
27
+ ]);
28
+
29
+ const LOW_PRIORITY_CHANCE = 0.3; // 30% for low priority
30
+
31
+ function init() {
32
+ if (!window.clawmate.onProactiveEvent) return;
33
+
34
+ window.clawmate.onProactiveEvent((event) => {
35
+ if (!enabled) return;
36
+ handleEvent(event);
37
+ });
38
+
39
+ // Load config
40
+ if (window.clawmate.getProactiveConfig) {
41
+ window.clawmate.getProactiveConfig().then((config) => {
42
+ enabled = config.enabled !== false;
43
+ });
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Handle a proactive event from main process
49
+ */
50
+ function handleEvent(event) {
51
+ const { trigger, context, timestamp } = event;
52
+ const now = Date.now();
53
+
54
+ // Global minimum gap between reactions (5s)
55
+ if (now - lastReactionTime < 5000) return;
56
+
57
+ // Don't react when sleeping
58
+ if (typeof StateMachine !== 'undefined' && StateMachine.getState() === 'sleeping') return;
59
+
60
+ // Probability check based on trigger priority
61
+ let chance = REACTION_CHANCE;
62
+ if (HIGH_PRIORITY.has(trigger)) {
63
+ chance = HIGH_PRIORITY_CHANCE;
64
+ } else if (LOW_PRIORITY.has(trigger)) {
65
+ chance = LOW_PRIORITY_CHANCE;
66
+ }
67
+ if (Math.random() > chance) return;
68
+
69
+ // Route based on AI connection status
70
+ const isAI = typeof AIController !== 'undefined' && AIController.isConnected();
71
+
72
+ if (isAI) {
73
+ // Main process에서 screen capture와 함께 직접 AI에 전송함
74
+ // Renderer에서는 추가 전송하지 않음 (중복 방지)
75
+ return;
76
+ } else {
77
+ showAutonomousReaction(trigger, context);
78
+ }
79
+
80
+ lastReactionTime = now;
81
+ }
82
+
83
+ /**
84
+ * AI mode: Send context to AI for decision
85
+ */
86
+ function reportToAI(trigger, context) {
87
+ if (!window.clawmate.reportToAI) return;
88
+
89
+ window.clawmate.reportToAI('proactive_trigger', {
90
+ trigger,
91
+ ...context,
92
+ timestamp: Date.now(),
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Autonomous mode: Show preset reaction
98
+ */
99
+ function showAutonomousReaction(trigger, context) {
100
+ const msgs = window._messages;
101
+ if (!msgs?.proactive) return;
102
+
103
+ const triggerData = msgs.proactive[trigger];
104
+ if (!triggerData?.messages?.length) return;
105
+
106
+ // Pick a random message
107
+ const message = triggerData.messages[
108
+ Math.floor(Math.random() * triggerData.messages.length)
109
+ ];
110
+
111
+ // Personalize message with context where possible
112
+ const finalMessage = personalizeMessage(message, trigger, context);
113
+
114
+ // Show speech bubble
115
+ if (typeof Speech !== 'undefined') {
116
+ Speech.show(finalMessage);
117
+ }
118
+
119
+ // Apply emotion
120
+ if (typeof StateMachine !== 'undefined' && triggerData.emotion) {
121
+ applyProactiveEmotion(triggerData.emotion);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Personalize generic messages with specific context
127
+ */
128
+ function personalizeMessage(message, trigger, context) {
129
+ // For app_switch, mention the new app
130
+ if (trigger === 'app_switch' && context.to) {
131
+ const templates = [
132
+ `Switching to ${context.to}!`,
133
+ `${context.to}? Let's see~`,
134
+ `Off to ${context.to}!`,
135
+ ];
136
+ return templates[Math.floor(Math.random() * templates.length)];
137
+ }
138
+
139
+ // For idle_return, mention duration if long
140
+ if (trigger === 'idle_return' && context.idleDuration > 300) {
141
+ const mins = Math.floor(context.idleDuration / 60);
142
+ return `Welcome back! You were gone for ${mins} minutes!`;
143
+ }
144
+
145
+ // For long_focus, mention duration
146
+ if (trigger === 'long_focus' && context.duration) {
147
+ const mins = Math.floor(context.duration / 60);
148
+ return `${mins} minutes focused! Take a stretch?`;
149
+ }
150
+
151
+ return message;
152
+ }
153
+
154
+ /**
155
+ * Map emotion string to pet state
156
+ */
157
+ function applyProactiveEmotion(emotion) {
158
+ const emotionMap = {
159
+ curious: 'walking',
160
+ excited: 'excited',
161
+ happy: 'excited',
162
+ worried: 'scared',
163
+ scared: 'scared',
164
+ playful: 'playing',
165
+ sleepy: 'sleeping',
166
+ neutral: null, // don't change state
167
+ };
168
+
169
+ const state = emotionMap[emotion];
170
+ if (!state) return;
171
+
172
+ const currentState = StateMachine.getState();
173
+ // Only change if in idle or walking state
174
+ if (currentState !== 'idle' && currentState !== 'walking') return;
175
+
176
+ StateMachine.forceState(state);
177
+
178
+ // Return to idle/walking after a short period (except sleeping)
179
+ if (state !== 'sleeping') {
180
+ setTimeout(() => {
181
+ if (StateMachine.getState() === state) {
182
+ StateMachine.forceState('idle');
183
+ }
184
+ }, 2000);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Get time of last reaction (for BrowserWatcher collision avoidance)
190
+ */
191
+ function getLastReactionTime() {
192
+ return lastReactionTime;
193
+ }
194
+
195
+ function setEnabled(val) {
196
+ enabled = val;
197
+ }
198
+
199
+ return {
200
+ init,
201
+ handleEvent,
202
+ getLastReactionTime,
203
+ setEnabled,
204
+ };
205
+ })();