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
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
package/preload/preload.js
CHANGED
|
@@ -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
|
});
|
package/renderer/index.html
CHANGED
package/renderer/js/app.js
CHANGED
|
@@ -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
|
+
})();
|