clawmate 1.3.0 → 1.4.1

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.
Files changed (43) hide show
  1. package/electron-builder.yml +1 -1
  2. package/index.js +589 -406
  3. package/main/ai-bridge.js +64 -58
  4. package/main/ai-connector.js +67 -62
  5. package/main/autostart.js +7 -7
  6. package/main/desktop-path.js +4 -4
  7. package/main/file-command-parser.js +77 -41
  8. package/main/file-ops.js +27 -27
  9. package/main/index.js +18 -16
  10. package/main/ipc-handlers.js +27 -24
  11. package/main/manifest.js +2 -2
  12. package/main/platform.js +16 -16
  13. package/main/smart-file-ops.js +64 -64
  14. package/main/store.js +1 -1
  15. package/main/telegram.js +154 -121
  16. package/main/tray.js +226 -71
  17. package/main/updater.js +13 -13
  18. package/openclaw.plugin.json +1 -1
  19. package/package.json +3 -4
  20. package/preload/preload.js +18 -18
  21. package/renderer/css/effects.css +6 -6
  22. package/renderer/css/pet.css +8 -8
  23. package/renderer/css/speech.css +5 -5
  24. package/renderer/first-run.html +15 -15
  25. package/renderer/index.html +4 -4
  26. package/renderer/js/ai-controller.js +99 -88
  27. package/renderer/js/app.js +26 -23
  28. package/renderer/js/browser-watcher.js +32 -32
  29. package/renderer/js/character.js +33 -33
  30. package/renderer/js/interactions.js +57 -14
  31. package/renderer/js/memory.js +144 -37
  32. package/renderer/js/metrics.js +141 -141
  33. package/renderer/js/mode-manager.js +59 -15
  34. package/renderer/js/pet-engine.js +236 -236
  35. package/renderer/js/speech.js +19 -19
  36. package/renderer/js/state-machine.js +23 -23
  37. package/renderer/js/time-aware.js +15 -15
  38. package/renderer/launcher.html +9 -9
  39. package/shared/constants.js +11 -11
  40. package/shared/messages.js +130 -130
  41. package/shared/personalities.js +72 -37
  42. package/skills/launch-pet/index.js +13 -13
  43. package/skills/launch-pet/skill.json +12 -23
package/index.js CHANGED
@@ -1,82 +1,86 @@
1
1
  /**
2
- * ClawMate OpenClaw 플러그인 진입점
2
+ * ClawMate plugin entry point
3
3
  *
4
- * 핵심 원칙: OpenClaw이 켜지면 자동으로 ClawMate를 찾아서 연결.
4
+ * Core principle: When AI connects, automatically find and connect to ClawMate.
5
5
  *
6
- * 흐름:
7
- * OpenClaw 시작 플러그인 로드 → init() 자동 호출
8
- * ClawMate 실행 중인지 확인 (ws://127.0.0.1:9320 연결 시도)
9
- * 실행 중이면: 바로 연결, AI 역할 시작
10
- * 돌고 있으면: Electron 자동 실행 → 연결
11
- * 연결 끊기면: 자동 재연결 (무한 반복)
6
+ * Flow:
7
+ * Plugin load -> init() auto-called
8
+ * -> Check if ClawMate is running (ws://127.0.0.1:9320 connection attempt)
9
+ * -> If running: connect immediately, start acting as AI brain
10
+ * -> If not running: auto-launch Electron app -> connect
11
+ * -> If disconnected: auto-reconnect (infinite retry)
12
12
  */
13
13
  const { spawn } = require('child_process');
14
14
  const path = require('path');
15
15
  const os = require('os');
16
16
  const fs = require('fs');
17
- const { OpenClawConnector } = require('./main/ai-connector');
17
+ const { ClawMateConnector } = require('./main/ai-connector');
18
18
 
19
19
  let connector = null;
20
20
  let electronProcess = null;
21
21
  let apiRef = null;
22
22
 
23
23
  // =====================================================
24
- // Think Loop 상태 관리
24
+ // Think Loop state management
25
25
  // =====================================================
26
26
  let thinkTimer = null;
27
27
  let lastSpeechTime = 0;
28
28
  let lastActionTime = 0;
29
29
  let lastDesktopCheckTime = 0;
30
30
  let lastScreenCheckTime = 0;
31
- let lastGreetingDate = null; // 하루에 한번만 인사
31
+ let lastGreetingDate = null; // Greet only once per day
32
32
 
33
- // 브라우징 감시 시스템 상태
33
+ // Browsing watch system state
34
34
  let browsingContext = {
35
- title: '', // 현재 윈도우 제목
36
- category: '', // 카테고리 (shopping, video, dev )
37
- lastCommentTime: 0, // 마지막 AI 코멘트 시각
38
- screenImage: null, // 최근 화면 캡처 (base64)
39
- cursorX: 0, // 커서 X 좌표
40
- cursorY: 0, // 커서 Y 좌표
35
+ title: '', // Current window title
36
+ category: '', // Category (shopping, video, dev, etc.)
37
+ lastCommentTime: 0, // Last AI comment timestamp
38
+ screenImage: null, // Latest screen capture (base64)
39
+ cursorX: 0, // Cursor X coordinate
40
+ cursorY: 0, // Cursor Y coordinate
41
41
  };
42
42
 
43
- // 공간 탐험 시스템 상태
44
- let knownWindows = []; // 알고 있는 윈도우 목록
43
+ // Spatial exploration system state
44
+ let knownWindows = []; // Known window list
45
45
  let lastWindowCheckTime = 0;
46
- let homePosition = null; // "" 위치 (자주 가는 곳)
47
- let explorationHistory = []; // 탐험한 위치 기록
46
+ let homePosition = null; // "Home" position (frequently visited)
47
+ let explorationHistory = []; // Exploration position history
48
48
  let lastExploreTime = 0;
49
49
  let lastFolderCarryTime = 0;
50
50
 
51
51
  // =====================================================
52
- // 자기 관찰 시스템 상태 (Metrics)
52
+ // Self-observation system state (Metrics)
53
53
  // =====================================================
54
- let latestMetrics = null; // 가장 최근 수신한 메트릭 데이터
55
- let metricsHistory = []; // 최근 10 메트릭 보고 이력
56
- let behaviorAdjustments = { // 현재 적용 중인 행동 조정값
57
- speechCooldownMultiplier: 1.0, // 말풍선 빈도 조절 (1.0=기본, >1=줄임, <1=늘림)
58
- actionCooldownMultiplier: 1.0, // 행동 빈도 조절
59
- explorationBias: 0, // 탐험 편향 (양수=더 탐험, 음수=덜 탐험)
60
- activityLevel: 1.0, // 활동 수준 (0.5=차분, 1.0=보통, 1.5=활발)
54
+ let latestMetrics = null; // Most recently received metrics data
55
+ let metricsHistory = []; // Last 10 metrics report history
56
+ let behaviorAdjustments = { // Currently applied behavior adjustments
57
+ speechCooldownMultiplier: 1.0, // Speech bubble frequency control (1.0=default, >1=less, <1=more)
58
+ actionCooldownMultiplier: 1.0, // Action frequency control
59
+ explorationBias: 0, // Exploration bias (positive=more, negative=less)
60
+ activityLevel: 1.0, // Activity level (0.5=calm, 1.0=normal, 1.5=active)
61
61
  };
62
- let lastMetricsLogTime = 0; // 마지막 품질 보고서 로그 시각
62
+ let lastMetricsLogTime = 0; // Last quality report log timestamp
63
+
64
+ // AI motion generation system state
65
+ let lastMotionGenTime = 0; // Last motion generation timestamp
66
+ let generatedMotionCount = 0; // Number of generated motions
63
67
 
64
68
  module.exports = {
65
69
  id: 'clawmate',
66
70
  name: 'ClawMate',
67
- version: '1.3.0',
68
- description: 'OpenClaw 데스크톱 - AI가 조종하는 살아있는 Claw',
71
+ version: '1.4.0',
72
+ description: 'ClawMate desktop pet - a living body controlled by AI',
69
73
 
70
74
  /**
71
- * OpenClaw이 플러그인을 로드할 때 자동 호출
72
- * ClawMate 자동 실행 + 자동 연결
75
+ * Auto-called when plugin loads
76
+ * -> Auto-launch ClawMate + auto-connect
73
77
  */
74
78
  async init(api) {
75
79
  apiRef = api;
76
- console.log('[ClawMate] 플러그인 초기화자동 연결 시작');
80
+ console.log('[ClawMate] Plugin initstarting auto-connect');
77
81
  autoConnect();
78
82
 
79
- // npm 패키지 버전 체크 (최초 1회 + 24시간 간격)
83
+ // npm package version check (once at start + every 24 hours)
80
84
  checkNpmUpdate();
81
85
  setInterval(checkNpmUpdate, 24 * 60 * 60 * 1000);
82
86
  },
@@ -84,137 +88,136 @@ module.exports = {
84
88
  register(api) {
85
89
  apiRef = api;
86
90
 
87
- // 실행 (이미 돌고 있으면 상태 알려줌)
91
+ // Launch pet (report status if already running)
88
92
  api.registerSkill('launch-pet', {
89
93
  triggers: [
90
- ' 깔아줘', ' 실행', ' 설치', ' 켜줘', ' 띄워줘',
91
- 'clawmate', 'clawmate 깔아줘', 'clawmate 설치', 'clawmate 켜줘',
92
- '클로메이트', '클로메이트 깔아줘', '데스크톱 펫',
93
- 'install pet', 'install clawmate', 'launch pet',
94
+ 'install pet', 'launch pet', 'start pet', 'run pet', 'open pet',
95
+ 'clawmate', 'install clawmate', 'launch clawmate', 'start clawmate',
96
+ 'desktop pet',
94
97
  ],
95
- description: '데스크톱 펫(ClawMate)을 실행하고 AI로 연결합니다',
98
+ description: 'Launch ClawMate desktop pet and connect to AI',
96
99
  execute: async () => {
97
100
  if (connector && connector.connected) {
98
- connector.speak('이미 여기 있어!');
101
+ connector.speak('Already here!');
99
102
  connector.action('excited');
100
- return { message: 'ClawMate 이미 실행 + AI 연결됨!' };
103
+ return { message: 'ClawMate already running + AI connected!' };
101
104
  }
102
105
  await ensureRunningAndConnected();
103
- return { message: 'ClawMate 실행 + AI 연결 완료!' };
106
+ return { message: 'ClawMate launched + AI connected!' };
104
107
  },
105
108
  });
106
109
 
107
- // 펫에게 말하기
110
+ // Speak through pet
108
111
  api.registerSkill('pet-speak', {
109
- triggers: ['펫한테 말해', '펫에게 전달', 'tell pet'],
110
- description: '펫을 통해 사용자에게 메시지를 전달합니다',
112
+ triggers: ['tell pet', 'say to pet', 'pet speak'],
113
+ description: 'Deliver a message to the user through the pet',
111
114
  execute: async (context) => {
112
115
  if (!connector || !connector.connected) {
113
- return { message: 'ClawMate 연결 중이 아닙니다. 잠시 후 다시 시도...' };
116
+ return { message: 'ClawMate not connected. Try again shortly...' };
114
117
  }
115
118
  const text = context.params?.text || context.input;
116
119
  connector.speak(text);
117
- return { message: `펫이 말합니다: "${text}"` };
120
+ return { message: `Pet says: "${text}"` };
118
121
  },
119
122
  });
120
123
 
121
- // 행동 제어
124
+ // Pet action control
122
125
  api.registerSkill('pet-action', {
123
- triggers: ['펫 행동', 'pet action'],
124
- description: '펫의 행동을 직접 제어합니다',
126
+ triggers: ['pet action'],
127
+ description: 'Directly control the pet\'s actions',
125
128
  execute: async (context) => {
126
- if (!connector || !connector.connected) return { message: '연결 대기 중...' };
129
+ if (!connector || !connector.connected) return { message: 'Waiting for connection...' };
127
130
  const action = context.params?.action || 'excited';
128
131
  connector.action(action);
129
- return { message: `펫 행동: ${action}` };
132
+ return { message: `Pet action: ${action}` };
130
133
  },
131
134
  });
132
135
 
133
- // AI 종합 의사결정
136
+ // AI comprehensive decision-making
134
137
  api.registerSkill('pet-decide', {
135
138
  triggers: [],
136
- description: 'AI 펫의 종합적 행동을 결정합니다',
139
+ description: 'AI decides the pet\'s comprehensive behavior',
137
140
  execute: async (context) => {
138
141
  if (!connector || !connector.connected) return;
139
142
  connector.decide(context.params);
140
143
  },
141
144
  });
142
145
 
143
- // 스마트 파일 정리 (텔레그램에서 트리거 가능)
146
+ // Smart file organization (can be triggered from Telegram)
144
147
  api.registerSkill('pet-file-organize', {
145
148
  triggers: [
146
- '바탕화면 정리', '파일 정리', '파일 옮겨',
147
149
  'organize desktop', 'clean desktop', 'move files',
150
+ 'tidy up desktop', 'sort files',
148
151
  ],
149
- description: '펫이 바탕화면 파일을 정리합니다',
152
+ description: 'Pet organizes desktop files',
150
153
  execute: async (context) => {
151
154
  if (!connector || !connector.connected) {
152
- return { message: 'ClawMate 연결 중이 아닙니다.' };
155
+ return { message: 'ClawMate not connected.' };
153
156
  }
154
157
  const text = context.params?.text || context.input;
155
158
  const { parseMessage } = require('./main/file-command-parser');
156
159
  const parsed = parseMessage(text);
157
160
 
158
161
  if (parsed.type === 'smart_file_op') {
159
- // smart_file_op 명령을 커넥터를 통해 Electron 측에 전달
162
+ // Forward smart_file_op command to Electron side via connector
160
163
  connector._send('smart_file_op', {
161
164
  command: parsed,
162
165
  fromPlugin: true,
163
166
  });
164
- return { message: `파일 정리 시작: ${text}` };
167
+ return { message: `File organization started: ${text}` };
165
168
  }
166
169
 
167
- return { message: '파일 정리 명령을 이해하지 못했습니다.' };
170
+ return { message: 'Could not understand the file organization command.' };
168
171
  },
169
172
  });
170
173
  },
171
174
 
172
175
  /**
173
- * OpenClaw 종료 정리
176
+ * Cleanup when plugin is destroyed
174
177
  */
175
178
  async destroy() {
176
- console.log('[ClawMate] 플러그인 정리');
179
+ console.log('[ClawMate] Plugin cleanup');
177
180
  stopThinkLoop();
178
181
  if (connector) {
179
182
  connector.disconnect();
180
183
  connector = null;
181
184
  }
182
- // Electron 앱은 종료하지 않음펫은 자율 모드로 계속 살아있음
185
+ // Do not terminate Electron app pet continues living in autonomous mode
183
186
  },
184
187
  };
185
188
 
186
189
  // =====================================================
187
- // 자동 연결 시스템
190
+ // Auto-connect system
188
191
  // =====================================================
189
192
 
190
193
  /**
191
- * OpenClaw 시작 자동으로 ClawMate 찾기/실행/연결
192
- * 무한 재시도ClawMate가 살아있는 항상 연결 유지
194
+ * Auto-find/launch/connect ClawMate on plugin start
195
+ * Infinite retryalways maintain connection as long as ClawMate is alive
193
196
  */
194
197
  async function autoConnect() {
195
- // 1단계: 이미 돌고 있는 ClawMate에 연결 시도
198
+ // Step 1: Try connecting to already running ClawMate
196
199
  const connected = await tryConnect();
197
200
  if (connected) {
198
- console.log('[ClawMate] 기존 ClawMate에 연결 성공');
201
+ console.log('[ClawMate] Connected to existing ClawMate');
199
202
  onConnected();
200
203
  return;
201
204
  }
202
205
 
203
- // 2단계: ClawMate 없으면 자동 실행
204
- console.log('[ClawMate] ClawMate 미감지자동 실행');
206
+ // Step 2: If ClawMate not found, auto-launch
207
+ console.log('[ClawMate] ClawMate not detected auto-launching');
205
208
  launchElectronApp();
206
209
 
207
- // 3단계: 실행될 때까지 대기 연결
210
+ // Step 3: Wait for launch then connect
208
211
  await waitAndConnect();
209
212
  }
210
213
 
211
214
  /**
212
- * WebSocket 연결 시도 (1회)
215
+ * WebSocket connection attempt (single try)
213
216
  */
214
217
  function tryConnect() {
215
218
  return new Promise((resolve) => {
216
219
  if (!connector) {
217
- connector = new OpenClawConnector(9320);
220
+ connector = new ClawMateConnector(9320);
218
221
  setupConnectorEvents();
219
222
  }
220
223
 
@@ -230,25 +233,25 @@ function tryConnect() {
230
233
  }
231
234
 
232
235
  /**
233
- * ClawMate 실행 대기 연결 (최대 30)
236
+ * Wait for ClawMate to start -> connect (max 30 seconds)
234
237
  */
235
238
  async function waitAndConnect() {
236
239
  for (let i = 0; i < 60; i++) {
237
240
  await sleep(500);
238
241
  const ok = await tryConnect();
239
242
  if (ok) {
240
- console.log('[ClawMate] 연결 성공');
243
+ console.log('[ClawMate] Connection successful');
241
244
  onConnected();
242
245
  return;
243
246
  }
244
247
  }
245
- console.log('[ClawMate] 30초 연결 실패백그라운드 재시도 시작');
248
+ console.log('[ClawMate] Connection failed within 30sstarting background retry');
246
249
  startBackgroundReconnect();
247
250
  }
248
251
 
249
252
  /**
250
- * 백그라운드 재연결 루프
251
- * 끊기면 10초마다 재시도
253
+ * Background reconnection loop
254
+ * Retry every 10 seconds on disconnect
252
255
  */
253
256
  let reconnectTimer = null;
254
257
 
@@ -262,7 +265,7 @@ function startBackgroundReconnect() {
262
265
  }
263
266
  const ok = await tryConnect();
264
267
  if (ok) {
265
- console.log('[ClawMate] 백그라운드 재연결 성공');
268
+ console.log('[ClawMate] Background reconnection successful');
266
269
  onConnected();
267
270
  clearInterval(reconnectTimer);
268
271
  reconnectTimer = null;
@@ -271,7 +274,7 @@ function startBackgroundReconnect() {
271
274
  }
272
275
 
273
276
  /**
274
- * 커넥터 이벤트 설정 (최초 1회)
277
+ * Setup connector events (once)
275
278
  */
276
279
  let eventsSetup = false;
277
280
  function setupConnectorEvents() {
@@ -282,18 +285,18 @@ function setupConnectorEvents() {
282
285
  await handleUserEvent(event);
283
286
  });
284
287
 
285
- // 메트릭 리포트 수신 자기 관찰 시스템에서 분석
288
+ // Receive metrics report -> analyze in self-observation system
286
289
  connector.onMetrics((data) => {
287
290
  handleMetrics(data);
288
291
  });
289
292
 
290
- // 윈도우 위치 정보 수신 탐험 시스템에서 활용
293
+ // Receive window position info -> used by exploration system
291
294
  connector.on('window_positions', (data) => {
292
295
  knownWindows = data.windows || [];
293
296
  });
294
297
 
295
298
  connector.on('disconnected', () => {
296
- console.log('[ClawMate] 연결 끊김 — Think Loop 중단, 재연결 시도');
299
+ console.log('[ClawMate] Disconnectedstopping Think Loop, retrying connection');
297
300
  stopThinkLoop();
298
301
  startBackgroundReconnect();
299
302
  });
@@ -304,17 +307,17 @@ function setupConnectorEvents() {
304
307
  }
305
308
 
306
309
  /**
307
- * 연결 성공
310
+ * On successful connection
308
311
  */
309
312
  function onConnected() {
310
313
  if (connector && connector.connected) {
311
- connector.speak('OpenClaw 연결됨! 같이 놀자!');
314
+ connector.speak('AI connected! Let\'s play!');
312
315
  connector.action('excited');
313
316
 
314
- // "" 위치 설정 화면 하단 왼쪽을 기본 홈으로
317
+ // Set "home" positionbottom-left of screen as default home
315
318
  homePosition = { x: 100, y: 1000, edge: 'bottom' };
316
319
 
317
- // 초기 윈도우 목록 조회
320
+ // Initial window list query
318
321
  connector.queryWindows();
319
322
 
320
323
  startThinkLoop();
@@ -322,7 +325,7 @@ function onConnected() {
322
325
  }
323
326
 
324
327
  // =====================================================
325
- // Electron 실행
328
+ // Electron app launch
326
329
  // =====================================================
327
330
 
328
331
  function launchElectronApp() {
@@ -331,7 +334,7 @@ function launchElectronApp() {
331
334
  const platform = os.platform();
332
335
  const appDir = path.resolve(__dirname);
333
336
 
334
- // 설치된 Electron 바이너리 확인
337
+ // Check for installed Electron binary
335
338
  const electronPaths = [
336
339
  path.join(appDir, 'node_modules', '.bin', platform === 'win32' ? 'electron.cmd' : 'electron'),
337
340
  path.join(appDir, 'node_modules', 'electron', 'dist', platform === 'win32' ? 'electron.exe' : 'electron'),
@@ -349,7 +352,7 @@ function launchElectronApp() {
349
352
  cwd: appDir,
350
353
  });
351
354
  } else {
352
- // npx 폴백
355
+ // npx fallback
353
356
  const npxCmd = platform === 'win32' ? 'npx.cmd' : 'npx';
354
357
  electronProcess = spawn(npxCmd, ['electron', appDir], {
355
358
  detached: true,
@@ -361,13 +364,13 @@ function launchElectronApp() {
361
364
  electronProcess.unref();
362
365
  electronProcess.on('exit', () => {
363
366
  electronProcess = null;
364
- // 펫이 죽으면 재시작 시도 (크래시 방어)
365
- console.log('[ClawMate] Electron 종료 감지');
367
+ // Attempt restart if pet dies (crash defense)
368
+ console.log('[ClawMate] Electron exit detected');
366
369
  });
367
370
  }
368
371
 
369
372
  // =====================================================
370
- // AI 이벤트 핸들링
373
+ // AI event handling
371
374
  // =====================================================
372
375
 
373
376
  async function handleUserEvent(event) {
@@ -387,8 +390,16 @@ async function handleUserEvent(event) {
387
390
  }
388
391
  break;
389
392
 
393
+ case 'double_click':
394
+ connector.decide({
395
+ action: 'excited',
396
+ speech: 'Wow! A double-click! Feels great~',
397
+ emotion: 'happy',
398
+ });
399
+ break;
400
+
390
401
  case 'drag':
391
- connector.speak('으앗, 나를 옮기다니!');
402
+ connector.speak('Whoa, you\'re moving me!');
392
403
  break;
393
404
 
394
405
  case 'desktop_changed':
@@ -396,7 +407,7 @@ async function handleUserEvent(event) {
396
407
  if (fileCount > 15) {
397
408
  connector.decide({
398
409
  action: 'walking',
399
- speech: '바탕화면이 복잡해 보이는데... 정리 도와줄까?',
410
+ speech: 'Desktop looks a bit messy... want me to help tidy up?',
400
411
  emotion: 'curious',
401
412
  });
402
413
  }
@@ -406,13 +417,13 @@ async function handleUserEvent(event) {
406
417
  if (event.hour === 23) {
407
418
  connector.decide({
408
419
  action: 'sleeping',
409
- speech: '슬슬 시간이야... 굿나잇!',
420
+ speech: 'Time to sleep soon... good night!',
410
421
  emotion: 'sleepy',
411
422
  });
412
423
  } else if (event.hour === 6) {
413
424
  connector.decide({
414
425
  action: 'excited',
415
- speech: '좋은 아침! 오늘도 화이팅!',
426
+ speech: 'Good morning! Let\'s crush it today!',
416
427
  emotion: 'happy',
417
428
  });
418
429
  }
@@ -426,7 +437,7 @@ async function handleUserEvent(event) {
426
437
  if (event.idleSeconds > 300) {
427
438
  connector.decide({
428
439
  action: 'idle',
429
- speech: '...자고 있는 아니지?',
440
+ speech: '...you\'re not sleeping, are you?',
430
441
  emotion: 'curious',
431
442
  });
432
443
  }
@@ -447,15 +458,15 @@ function sleep(ms) {
447
458
  }
448
459
 
449
460
  // =====================================================
450
- // 브라우징 AI 코멘트 시스템
451
- // 윈도우 제목 + 화면 캡처 + 커서 위치 기반 맥락 코멘트
461
+ // Browsing AI comment system
462
+ // Contextual comments based on window title + screen capture + cursor position
452
463
  // =====================================================
453
464
 
454
465
  /**
455
- * 브라우징 컨텍스트 수신 AI 코멘트 생성
466
+ * Receive browsing context -> generate AI comment
456
467
  *
457
- * 렌더러(BrowserWatcher)가 감지한 브라우징 활동을 받아서
458
- * 화면 캡처와 제목을 분석하여 맥락에 맞는 코멘트를 생성한다.
468
+ * Receives browsing activity detected by the renderer (BrowserWatcher)
469
+ * and generates contextual comments by analyzing screen capture and title.
459
470
  *
460
471
  * @param {object} event - { title, category, cursorX, cursorY, screen?, titleChanged }
461
472
  */
@@ -463,7 +474,7 @@ async function handleBrowsingComment(event) {
463
474
  if (!connector || !connector.connected) return;
464
475
 
465
476
  const now = Date.now();
466
- // AI 코멘트 쿨다운 (45)
477
+ // AI comment cooldown (45 seconds)
467
478
  if (now - browsingContext.lastCommentTime < 45000) return;
468
479
 
469
480
  browsingContext.title = event.title || '';
@@ -471,31 +482,31 @@ async function handleBrowsingComment(event) {
471
482
  browsingContext.cursorX = event.cursorX || 0;
472
483
  browsingContext.cursorY = event.cursorY || 0;
473
484
 
474
- // 화면 캡처 데이터 저장 (있으면)
485
+ // Save screen capture data (if available)
475
486
  if (event.screen?.image) {
476
487
  browsingContext.screenImage = event.screen.image;
477
488
  }
478
489
 
479
490
  let comment = null;
480
491
 
481
- // 1차: apiRef.generate()로 AI 텍스트 생성 시도
492
+ // Attempt 1: AI text generation via apiRef.generate()
482
493
  if (apiRef?.generate) {
483
494
  try {
484
495
  const prompt = buildBrowsingPrompt(event);
485
496
  comment = await apiRef.generate(prompt);
486
- // 너무 응답은 자르기
497
+ // Truncate overly long responses
487
498
  if (comment && comment.length > 50) {
488
499
  comment = comment.slice(0, 50);
489
500
  }
490
501
  } catch {}
491
502
  }
492
503
 
493
- // 2차: apiRef.chat()으로 시도
504
+ // Attempt 2: try via apiRef.chat()
494
505
  if (!comment && apiRef?.chat) {
495
506
  try {
496
507
  const prompt = buildBrowsingPrompt(event);
497
508
  const response = await apiRef.chat([
498
- { role: 'system', content: ' 데스크톱 위의 작은 펫이야. 짧고 재치있게 한마디 해. 20 이내. 한국어.' },
509
+ { role: 'system', content: 'You are a small pet on the desktop. Say something short and witty. Under 20 words. English.' },
499
510
  { role: 'user', content: prompt },
500
511
  ]);
501
512
  comment = response?.text || response?.content || response;
@@ -505,16 +516,16 @@ async function handleBrowsingComment(event) {
505
516
  } catch {}
506
517
  }
507
518
 
508
- // 3차: 이미지 분석으로 시도 (화면 캡처가 있을 )
519
+ // Attempt 3: try via image analysis (when screen capture is available)
509
520
  if (!comment && apiRef?.analyzeImage && browsingContext.screenImage) {
510
521
  try {
511
522
  comment = await apiRef.analyzeImage(browsingContext.screenImage, {
512
- prompt: `사용자가 "${browsingContext.title}" 보고 있어. 커서 위치: (${browsingContext.cursorX}, ${browsingContext.cursorY}). 데스크톱 펫으로서 화면 내용에 대해 재치있게 한마디 해줘. 20 이내. 한국어.`,
523
+ prompt: `User is viewing "${browsingContext.title}". Cursor position: (${browsingContext.cursorX}, ${browsingContext.cursorY}). As a desktop pet, make a witty one-liner about the screen content. Under 20 words. English.`,
513
524
  });
514
525
  } catch {}
515
526
  }
516
527
 
517
- // 4차: 스마트 폴백타이틀 분석 기반 코멘트
528
+ // Attempt 4: Smart fallbacktitle analysis based comment
518
529
  if (!comment || typeof comment !== 'string') {
519
530
  comment = generateSmartBrowsingComment(browsingContext);
520
531
  }
@@ -527,134 +538,134 @@ async function handleBrowsingComment(event) {
527
538
  });
528
539
  browsingContext.lastCommentTime = now;
529
540
  lastSpeechTime = now;
530
- console.log(`[ClawMate] 브라우징 코멘트: ${comment}`);
541
+ console.log(`[ClawMate] Browsing comment: ${comment}`);
531
542
 
532
- // 1.5 후 원래 상태로
543
+ // Return to normal state after 1.5 seconds
533
544
  setTimeout(() => {
534
545
  if (connector?.connected) connector.action('idle');
535
546
  }, 1500);
536
547
  }
537
548
 
538
- // 캡처 데이터 정리 (메모리 절약)
549
+ // Clean up capture data (memory savings)
539
550
  browsingContext.screenImage = null;
540
551
  }
541
552
 
542
553
  /**
543
- * AI 코멘트 생성용 프롬프트 구성
554
+ * Build prompt for AI comment generation
544
555
  */
545
556
  function buildBrowsingPrompt(event) {
546
557
  const title = event.title || '';
547
558
  const category = event.category || 'unknown';
548
559
  const cursor = event.cursorX && event.cursorY
549
- ? `커서 위치: (${event.cursorX}, ${event.cursorY}).`
560
+ ? `Cursor position: (${event.cursorX}, ${event.cursorY}).`
550
561
  : '';
551
562
 
552
- return `사용자가 지금 "${title}" 화면을 보고 있어. ` +
553
- `카테고리: ${category}. ${cursor} ` +
554
- `이 상황에 대해 짧고 재치있게 한마디 해줘. 20 이내. 한국어로. ` +
555
- `너는 데스크톱 위의 작은 귀여운 펫이야. 친근하고 장난스러운 톤으로.`;
563
+ return `User is currently viewing "${title}". ` +
564
+ `Category: ${category}. ${cursor} ` +
565
+ `Say something short and witty about this. Under 20 words. English. ` +
566
+ `You are a cute little pet on the desktop. Friendly and playful tone.`;
556
567
  }
557
568
 
558
569
  /**
559
- * 타이틀 분석 기반 스마트 코멘트 생성
570
+ * Smart comment generation based on title analysis
560
571
  *
561
- * AI API가 없어도 윈도우 제목에서 실제 맥락을 추출하여
562
- * 프리셋보다 훨씬 자연스러운 코멘트를 생성한다.
572
+ * Extracts real context from window title even without AI API
573
+ * to generate much more natural comments than presets.
563
574
  *
564
- * 예: "React hooks tutorial - YouTube" "리액트 공부하고 있구나!"
565
- * "Pull Request #42 - GitHub" "PR 리뷰 중? 꼼꼼히 봐!"
575
+ * e.g.: "React hooks tutorial - YouTube" -> "Studying React hooks!"
576
+ * "Pull Request #42 - GitHub" -> "Reviewing a PR? Look carefully!"
566
577
  */
567
578
  function generateSmartBrowsingComment(ctx) {
568
579
  const title = ctx.title || '';
569
580
  const category = ctx.category || '';
570
581
  const titleLower = title.toLowerCase();
571
582
 
572
- // 타이틀에서 사이트명과 페이지 제목 분리
573
- // 일반적 패턴: "페이지 제목 - 사이트명" or "사이트명: 페이지 제목"
574
- const parts = title.split(/\s[-–|:]\s/);
583
+ // Separate site name and page title from the title
584
+ // Common pattern: "Page Title - Site Name" or "Site Name: Page Title"
585
+ const parts = title.split(/\s[-\u2013|:]\s/);
575
586
  const pageName = (parts[0] || title).trim();
576
587
  const pageShort = pageName.slice(0, 20);
577
588
 
578
- // 카테고리별 맥락 인식 코멘트 생성기
589
+ // Category-specific contextual comment generators
579
590
  const generators = {
580
591
  shopping: () => {
581
592
  const templates = [
582
- `${pageShort} 보고 있구나? 좋은 찾으면 알려줘!`,
583
- `쇼핑 중이네! ${pageShort}... 거야?`,
584
- `${pageShort} 괜찮아 보이는데? 장바구니 담을 거야?`,
593
+ `Browsing ${pageShort}? Let me know if you find something good!`,
594
+ `Shopping! ${pageShort}... buying it?`,
595
+ `${pageShort} looks nice? Adding to cart?`,
585
596
  ];
586
597
  return pick(templates);
587
598
  },
588
599
  video: () => {
589
- if (titleLower.includes('youtube') || titleLower.includes('유튜브')) {
590
- return `"${pageShort}" 재미있어? 나도 궁금!`;
600
+ if (titleLower.includes('youtube')) {
601
+ return `"${pageShort}" any good? I'm curious!`;
591
602
  }
592
- if (titleLower.includes('netflix') || titleLower.includes('넷플릭스') ||
603
+ if (titleLower.includes('netflix') ||
593
604
  titleLower.includes('tving') || titleLower.includes('watcha')) {
594
- return `뭐 보는 거야? "${pageShort}" 재밌어?`;
605
+ return `What are you watching? "${pageShort}" fun?`;
595
606
  }
596
- return `영상 보고 있구나! "${pageShort}" 추천할 만해?`;
607
+ return `Watching videos! "${pageShort}" worth recommending?`;
597
608
  },
598
609
  sns: () => {
599
610
  if (titleLower.includes('twitter') || titleLower.includes('x.com')) {
600
- return '트윗 보고 있구나~ 재미있는 거 있어?';
611
+ return 'Scrolling through tweets~ anything interesting?';
601
612
  }
602
- if (titleLower.includes('instagram') || titleLower.includes('인스타')) {
603
- return '인스타 구경 중? 좋은 사진 보여줘!';
613
+ if (titleLower.includes('instagram')) {
614
+ return 'Browsing Insta? Show me cool pics!';
604
615
  }
605
616
  if (titleLower.includes('reddit')) {
606
- return '레딧 탐색 중! 어떤 서브레딧이야?';
617
+ return 'Exploring Reddit! Which subreddit?';
607
618
  }
608
- return 'SNS 하고 있구나~ 무한 스크롤 조심!';
619
+ return 'On social media~ watch out for infinite scroll!';
609
620
  },
610
621
  news: () => {
611
- return `"${pageShort}" 무슨 뉴스야? 좋은 소식이길!`;
622
+ return `"${pageShort}" \u2014 what's the news? Hope it's good!`;
612
623
  },
613
624
  dev: () => {
614
625
  if (titleLower.includes('pull request') || titleLower.includes('pr #')) {
615
- return 'PR 리뷰 중이구나! 꼼꼼히 봐~';
626
+ return 'Reviewing a PR! Look carefully~';
616
627
  }
617
628
  if (titleLower.includes('issue')) {
618
- return '이슈 처리 중? 화이팅!';
629
+ return 'Working on an issue? You got this!';
619
630
  }
620
631
  if (titleLower.includes('stackoverflow') || titleLower.includes('stack overflow')) {
621
- return '스택오버플로우! 뭐가 막혔어? 도와줄까?';
632
+ return 'Stack Overflow! What are you stuck on? Need help?';
622
633
  }
623
634
  if (titleLower.includes('github')) {
624
- return `GitHub에서 "${pageShort}" 작업 중?`;
635
+ return `Working on "${pageShort}" on GitHub?`;
625
636
  }
626
637
  if (titleLower.includes('docs') || titleLower.includes('documentation')) {
627
- return '문서 읽고 있구나! 공부 열심히~';
638
+ return 'Reading docs! Keep studying hard~';
628
639
  }
629
- return `코딩 관련! "${pageShort}" 화이팅!`;
640
+ return `Coding stuff! "${pageShort}" you got this!`;
630
641
  },
631
642
  search: () => {
632
- // "검색어 - Google 검색" 패턴에서 검색어 추출
633
- const searchMatch = title.match(/(.+?)\s*[-–]\s*(Google|Bing|네이버|Naver|검색)/i);
643
+ // Extract search query from "query - Google Search" pattern
644
+ const searchMatch = title.match(/(.+?)\s*[-\u2013]\s*(Google|Bing|Naver|Search)/i);
634
645
  if (searchMatch) {
635
646
  const query = searchMatch[1].trim().slice(0, 15);
636
647
  const templates = [
637
- `"${query}" 궁금해? 내가 알려줄 수도 있는데!`,
638
- `"${query}" 검색하고 있구나~ 찾으면 알려줘!`,
639
- `오, "${query}" 나도 궁금하다!`,
648
+ `Curious about "${query}"? I might know the answer!`,
649
+ `Searching "${query}"~ let me know what you find!`,
650
+ `Oh, "${query}" I'm curious too!`,
640
651
  ];
641
652
  return pick(templates);
642
653
  }
643
- return ' 찾고 있어? 궁금한 있으면 물어봐!';
654
+ return 'What are you looking for? Ask me if you need help!';
644
655
  },
645
656
  game: () => {
646
- return `${pageShort} 하고 있어? 이기고 있어?!`;
657
+ return `Playing ${pageShort}? Are you winning?!`;
647
658
  },
648
659
  music: () => {
649
- return `뭐 듣고 있어? "${pageShort}" 좋은 노래야?`;
660
+ return `What are you listening to? "${pageShort}" a good song?`;
650
661
  },
651
662
  mail: () => {
652
- return '메일 확인 중~ 중요한 거 있어?';
663
+ return 'Checking emails~ anything important?';
653
664
  },
654
665
  general: () => {
655
666
  const templates = [
656
- `"${pageShort}" 보고 있구나~`,
657
- `오, ${pageShort}! 하는 거야?`,
667
+ `Browsing "${pageShort}"~`,
668
+ `Oh, ${pageShort}! What's that about?`,
658
669
  ];
659
670
  return pick(templates);
660
671
  },
@@ -663,12 +674,12 @@ function generateSmartBrowsingComment(ctx) {
663
674
  const gen = generators[category];
664
675
  if (gen) return gen();
665
676
 
666
- // 카테고리 미매칭: 제목 기반 일반 코멘트
677
+ // No category match: general comment based on title
667
678
  if (pageName.length > 3) {
668
679
  const templates = [
669
- `"${pageShort}" 보고 있구나!`,
670
- `오, ${pageShort}! 재미있어?`,
671
- `${pageShort}... 하는 거야?`,
680
+ `Checking out "${pageShort}"!`,
681
+ `Oh, ${pageShort}! Looks interesting?`,
682
+ `${pageShort}... what's going on?`,
672
683
  ];
673
684
  return pick(templates);
674
685
  }
@@ -676,21 +687,21 @@ function generateSmartBrowsingComment(ctx) {
676
687
  return null;
677
688
  }
678
689
 
679
- /** 배열에서 랜덤 선택 */
690
+ /** Random pick from array */
680
691
  function pick(arr) {
681
692
  return arr[Math.floor(Math.random() * arr.length)];
682
693
  }
683
694
 
684
695
  // =====================================================
685
- // AI 캐릭터 생성 시스템
686
- // 텔레그램 컨셉 설명 AI 16x16 픽셀 아트 생성
696
+ // AI character generation system
697
+ // Concept description from Telegram -> AI generates 16x16 pixel art
687
698
  // =====================================================
688
699
 
689
700
  /**
690
- * 캐릭터 생성 요청 처리 (텔레그램에서 트리거)
701
+ * Handle character generation request (triggered from Telegram)
691
702
  *
692
- * 1차: apiRef로 AI 캐릭터 생성 (색상 + 프레임 데이터)
693
- * 2차: 키워드 기반 색상 변환 (AI 없을 폴백)
703
+ * Attempt 1: AI character generation via apiRef (colors + frame data)
704
+ * Attempt 2: Keyword-based color conversion (fallback when no AI)
694
705
  *
695
706
  * @param {object} event - { concept, chatId }
696
707
  */
@@ -700,45 +711,45 @@ async function handleCharacterRequest(event) {
700
711
  const concept = event.concept || '';
701
712
  if (!concept) return;
702
713
 
703
- console.log(`[ClawMate] 캐릭터 생성 요청: "${concept}"`);
714
+ console.log(`[ClawMate] Character generation request: "${concept}"`);
704
715
 
705
716
  let characterData = null;
706
717
 
707
- // 1차: AI로 색상 팔레트 + 프레임 데이터 생성
718
+ // Attempt 1: Generate color palette + frame data via AI
708
719
  if (apiRef?.generate) {
709
720
  try {
710
721
  characterData = await generateCharacterWithAI(concept);
711
722
  } catch (err) {
712
- console.log(`[ClawMate] AI 캐릭터 생성 실패: ${err.message}`);
723
+ console.log(`[ClawMate] AI character generation failed: ${err.message}`);
713
724
  }
714
725
  }
715
726
 
716
- // 2차: AI chat으로 시도
727
+ // Attempt 2: try via AI chat
717
728
  if (!characterData && apiRef?.chat) {
718
729
  try {
719
730
  characterData = await generateCharacterWithChat(concept);
720
731
  } catch (err) {
721
- console.log(`[ClawMate] AI chat 캐릭터 생성 실패: ${err.message}`);
732
+ console.log(`[ClawMate] AI chat character generation failed: ${err.message}`);
722
733
  }
723
734
  }
724
735
 
725
- // 3차: 키워드 기반 색상만 변환 (폴백)
736
+ // Attempt 3: keyword-based color conversion only (fallback)
726
737
  if (!characterData) {
727
738
  characterData = generateCharacterFromKeywords(concept);
728
739
  }
729
740
 
730
741
  if (characterData) {
731
- // 캐릭터 데이터를 렌더러에 전송
742
+ // Send character data to renderer
732
743
  connector._send('set_character', {
733
744
  ...characterData,
734
- speech: `${concept} 변신 완료!`,
745
+ speech: `${concept} transformation complete!`,
735
746
  });
736
- console.log(`[ClawMate] 캐릭터 생성 완료: "${concept}"`);
747
+ console.log(`[ClawMate] Character generation complete: "${concept}"`);
737
748
  }
738
749
  }
739
750
 
740
751
  /**
741
- * AI generate()로 캐릭터 생성
752
+ * Generate character via AI generate()
742
753
  */
743
754
  async function generateCharacterWithAI(concept) {
744
755
  const prompt = buildCharacterPrompt(concept);
@@ -747,12 +758,12 @@ async function generateCharacterWithAI(concept) {
747
758
  }
748
759
 
749
760
  /**
750
- * AI chat()으로 캐릭터 생성
761
+ * Generate character via AI chat()
751
762
  */
752
763
  async function generateCharacterWithChat(concept) {
753
764
  const prompt = buildCharacterPrompt(concept);
754
765
  const response = await apiRef.chat([
755
- { role: 'system', content: ' 16x16 픽셀 아트 캐릭터 디자이너야. JSON으로 캐릭터 데이터를 출력해.' },
766
+ { role: 'system', content: 'You are a 16x16 pixel art character designer. Output character data as JSON.' },
756
767
  { role: 'user', content: prompt },
757
768
  ]);
758
769
  const text = response?.text || response?.content || response;
@@ -760,42 +771,42 @@ async function generateCharacterWithChat(concept) {
760
771
  }
761
772
 
762
773
  /**
763
- * 캐릭터 생성 프롬프트
774
+ * Character generation prompt
764
775
  */
765
776
  function buildCharacterPrompt(concept) {
766
- return `"${concept}" 컨셉의 16x16 픽셀 아트 캐릭터를 만들어줘.
777
+ return `Create a 16x16 pixel art character with the concept "${concept}".
767
778
 
768
- JSON 형식으로 출력해:
779
+ Output as JSON:
769
780
  {
770
781
  "colorMap": {
771
- "primary": "#hex색상", // 메인 몸통
772
- "secondary": "#hex색상", // 보조 (배, )
773
- "dark": "#hex색상", // 어두운 부분 (다리, 그림자)
774
- "eye": "#hex색상", // 흰자
775
- "pupil": "#hex색상", // 눈동자
776
- "claw": "#hex색상" // 집게/손/특징 부위
782
+ "primary": "#hexcolor", // Main body color
783
+ "secondary": "#hexcolor", // Secondary color (belly, cheeks, etc.)
784
+ "dark": "#hexcolor", // Dark parts (legs, shadows)
785
+ "eye": "#hexcolor", // Eye whites
786
+ "pupil": "#hexcolor", // Pupil
787
+ "claw": "#hexcolor" // Claws/hands/feature parts
777
788
  },
778
789
  "frames": {
779
790
  "idle": [
780
- [16x16 숫자 배열 - frame 0],
781
- [16x16 숫자 배열 - frame 1]
791
+ [16x16 number array - frame 0],
792
+ [16x16 number array - frame 1]
782
793
  ]
783
794
  }
784
795
  }
785
796
 
786
- 숫자 의미: 0=투명, 1=primary, 2=secondary, 3=dark, 4=eye, 5=pupil, 6=claw
787
- 캐릭터는 (4+5), 몸통(1+2), 다리(3), 특징(6)을 포함해야 해.
788
- idle 프레임 2개만 만들어줘. 귀엽게!
789
- JSON 출력해.`;
797
+ Number meanings: 0=transparent, 1=primary, 2=secondary, 3=dark, 4=eye, 5=pupil, 6=claw
798
+ Character must include eyes(4+5), body(1+2), legs(3), features(6).
799
+ Create only 2 idle frames. Make it cute!
800
+ Output JSON only.`;
790
801
  }
791
802
 
792
803
  /**
793
- * AI 응답에서 캐릭터 데이터 파싱
804
+ * Parse character data from AI response
794
805
  */
795
806
  function parseCharacterResponse(response) {
796
807
  if (!response || typeof response !== 'string') return null;
797
808
 
798
- // JSON 블록 추출 (```json ... ``` 또는 { ... })
809
+ // Extract JSON block (```json ... ``` or { ... })
799
810
  let jsonStr = response;
800
811
  const jsonMatch = response.match(/```(?:json)?\s*([\s\S]*?)```/);
801
812
  if (jsonMatch) {
@@ -810,7 +821,7 @@ function parseCharacterResponse(response) {
810
821
  try {
811
822
  const data = JSON.parse(jsonStr);
812
823
 
813
- // colorMap 검증
824
+ // Validate colorMap
814
825
  if (data.colorMap) {
815
826
  const required = ['primary', 'secondary', 'dark', 'eye', 'pupil', 'claw'];
816
827
  for (const key of required) {
@@ -820,11 +831,11 @@ function parseCharacterResponse(response) {
820
831
  return null;
821
832
  }
822
833
 
823
- // frames 검증 (있으면)
834
+ // Validate frames (if present)
824
835
  if (data.frames?.idle) {
825
836
  for (const frame of data.frames.idle) {
826
837
  if (!Array.isArray(frame) || frame.length !== 16) {
827
- delete data.frames; // 프레임 데이터 불량 색상만 사용
838
+ delete data.frames; // Bad frame data -> use colors only
828
839
  break;
829
840
  }
830
841
  for (const row of frame) {
@@ -839,10 +850,10 @@ function parseCharacterResponse(response) {
839
850
 
840
851
  return data;
841
852
  } catch {
842
- // JSON 파싱 실패 색상만 추출 시도
853
+ // JSON parsing failed -> attempt color extraction only
843
854
  const colorMatch = response.match(/"primary"\s*:\s*"(#[0-9a-fA-F]{6})"/);
844
855
  if (colorMatch) {
845
- // 최소한 primary 색상이라도 추출
856
+ // Extract at least the primary color
846
857
  return generateCharacterFromKeywords(response);
847
858
  }
848
859
  return null;
@@ -850,14 +861,14 @@ function parseCharacterResponse(response) {
850
861
  }
851
862
 
852
863
  /**
853
- * 키워드 기반 캐릭터 색상 생성 (AI 없을 폴백)
864
+ * Keyword-based character color generation (fallback when no AI)
854
865
  *
855
- * 컨셉에서 색상/생물 키워드를 추출하여 팔레트 생성
866
+ * Extract color/creature keywords from concept to generate palette
856
867
  */
857
868
  function generateCharacterFromKeywords(concept) {
858
869
  const c = (concept || '').toLowerCase();
859
870
 
860
- // 색상 키워드 매핑
871
+ // Color keyword mapping
861
872
  const colorMap = {
862
873
  '파란|파랑|blue': { primary: '#4488ff', secondary: '#6699ff', dark: '#223388', claw: '#4488ff' },
863
874
  '초록|녹색|green': { primary: '#44cc44', secondary: '#66dd66', dark: '#226622', claw: '#44cc44' },
@@ -870,7 +881,7 @@ function generateCharacterFromKeywords(concept) {
870
881
  '민트|틸|teal': { primary: '#00BFA5', secondary: '#33D4BC', dark: '#006655', claw: '#00BFA5' },
871
882
  };
872
883
 
873
- // 생물 키워드 매핑
884
+ // Creature keyword mapping
874
885
  const creatureMap = {
875
886
  '고양이|cat': { primary: '#ff9944', secondary: '#ffbb66', dark: '#663300', claw: '#ff9944' },
876
887
  '로봇|robot': { primary: '#888888', secondary: '#aaaaaa', dark: '#444444', claw: '#66aaff' },
@@ -886,7 +897,7 @@ function generateCharacterFromKeywords(concept) {
886
897
  '얼음|ice': { primary: '#88ccff', secondary: '#bbddff', dark: '#446688', claw: '#aaddff' },
887
898
  };
888
899
 
889
- // 색상 키워드 먼저 체크
900
+ // Check color keywords first
890
901
  for (const [keywords, palette] of Object.entries(colorMap)) {
891
902
  for (const kw of keywords.split('|')) {
892
903
  if (c.includes(kw)) {
@@ -897,7 +908,7 @@ function generateCharacterFromKeywords(concept) {
897
908
  }
898
909
  }
899
910
 
900
- // 생물 키워드 체크
911
+ // Check creature keywords
901
912
  for (const [keywords, palette] of Object.entries(creatureMap)) {
902
913
  for (const kw of keywords.split('|')) {
903
914
  if (c.includes(kw)) {
@@ -908,7 +919,7 @@ function generateCharacterFromKeywords(concept) {
908
919
  }
909
920
  }
910
921
 
911
- // 매칭 실패 랜덤 색상
922
+ // No match -> random color
912
923
  const hue = Math.floor(Math.random() * 360);
913
924
  const s = 70, l = 55;
914
925
  return {
@@ -923,7 +934,7 @@ function generateCharacterFromKeywords(concept) {
923
934
  };
924
935
  }
925
936
 
926
- /** HSL HEX 변환 */
937
+ /** HSL to HEX conversion */
927
938
  function hslToHex(h, s, l) {
928
939
  s /= 100;
929
940
  l /= 100;
@@ -937,56 +948,56 @@ function hslToHex(h, s, l) {
937
948
  }
938
949
 
939
950
  // =====================================================
940
- // AI Think Loop — 주기적 자율 사고 시스템
951
+ // AI Think Loop — periodic autonomous thinking system
941
952
  // =====================================================
942
953
 
943
- // 시간대별 인사말
954
+ // Time-based greetings
944
955
  const TIME_GREETINGS = {
945
956
  morning: [
946
- '좋은 아침! 오늘 하루도 화이팅!',
947
- '일어났어? 커피 어때?',
948
- '모닝~ 오늘 날씨 어떨까?',
957
+ 'Good morning! Let\'s make today great!',
958
+ 'You\'re up? How about a cup of coffee?',
959
+ 'Morning~ I wonder what the weather\'s like?',
949
960
  ],
950
961
  lunch: [
951
- '점심 시간이다! 먹을 거야?',
952
- ' 먹었어? 건강이 최고야!',
953
- '슬슬 배고프지 않아?',
962
+ 'Lunchtime! What are you going to eat?',
963
+ 'Had your meal? Health is wealth!',
964
+ 'Getting hungry yet?',
954
965
  ],
955
966
  evening: [
956
- '오늘 하루 수고했어!',
957
- '저녁이네~ 오늘 했어?',
958
- '하루가 벌써 이렇게 지나가다니...',
967
+ 'Great work today!',
968
+ 'It\'s evening~ what did you do today?',
969
+ 'Can\'t believe the day went by so fast...',
959
970
  ],
960
971
  night: [
961
- ' 시간까지 깨있는 거야? 자야지~',
962
- '밤이 깊었어... 내일도 있잖아.',
963
- '나는 슬슬 졸리다... zzZ',
972
+ 'Still up at this hour? You should sleep soon~',
973
+ 'It\'s getting late... tomorrow\'s another day.',
974
+ 'I\'m getting sleepy... zzZ',
964
975
  ],
965
976
  };
966
977
 
967
- // 한가할 혼잣말 목록
978
+ // Idle self-talk list
968
979
  const IDLE_CHATTER = [
969
- '음~ 하고 놀까...',
970
- '심심하다...',
971
- ' 여기 있는 알지?',
972
- '바탕화면 구경 중~',
973
- '오늘 기분이 좋다!',
974
- '후후, 잠깐 스트레칭~',
975
- '이리저리 돌아다녀볼까~',
976
- '혼자 놀기 프로...',
977
- '주인님 하고 있는 거야~?',
978
- '나한테 관심 줘봐!',
979
- '데스크톱이 넓고 좋다~',
980
- '여기서 보이는 세상!',
981
- // 공간 탐험 관련 멘트
982
- '화면 위를 점프해볼까~!',
983
- '천장에서 내려가보자!',
984
- ' 위에 올라가봐야지~',
985
- '여기가 집이야~ 편하다!',
986
- ' 돌아다녀볼까? 탐험 모드!',
980
+ 'Hmm~ what should I do...',
981
+ 'So bored...',
982
+ 'You know I\'m here, right?',
983
+ 'Exploring the desktop~',
984
+ 'Feeling good today!',
985
+ 'Hehe, time for a quick stretch~',
986
+ 'Maybe I\'ll wander around~',
987
+ 'Pro at entertaining myself...',
988
+ 'What are you up to~?',
989
+ 'Pay some attention to me!',
990
+ 'The desktop is nice and spacious~',
991
+ 'Everything I see is my world!',
992
+ // Spatial exploration lines
993
+ 'Should I jump across the screen~!',
994
+ 'Let me rappel down from up here!',
995
+ 'Gotta climb on top of this window~',
996
+ 'This is my home~ so comfy!',
997
+ 'Wanna explore? Adventure mode!',
987
998
  ];
988
999
 
989
- // 랜덤 행동 목록
1000
+ // Random action list
990
1001
  const RANDOM_ACTIONS = [
991
1002
  { action: 'walking', weight: 30, minInterval: 5000 },
992
1003
  { action: 'idle', weight: 25, minInterval: 3000 },
@@ -994,20 +1005,20 @@ const RANDOM_ACTIONS = [
994
1005
  { action: 'climbing', weight: 8, minInterval: 20000 },
995
1006
  { action: 'looking_around', weight: 20, minInterval: 8000 },
996
1007
  { action: 'sleeping', weight: 7, minInterval: 60000 },
997
- // 공간 이동 행동
1008
+ // Spatial movement actions
998
1009
  { action: 'jumping', weight: 5, minInterval: 30000 },
999
1010
  { action: 'rappelling', weight: 3, minInterval: 45000 },
1000
1011
  ];
1001
1012
 
1002
1013
  /**
1003
- * Think Loop 시작
1004
- * 3초 간격으로 AI가 자율적으로 사고하고 행동을 결정
1014
+ * Start Think Loop
1015
+ * AI autonomously thinks and decides actions every 3 seconds
1005
1016
  */
1006
1017
  function startThinkLoop() {
1007
1018
  if (thinkTimer) return;
1008
- console.log('[ClawMate] Think Loop 시작3초 간격 자율 사고');
1019
+ console.log('[ClawMate] Think Loop started3s interval autonomous thinking');
1009
1020
 
1010
- // 초기 타임스탬프 설정 (시작 직후 스팸 방지)
1021
+ // Set initial timestamps (prevent spam right after start)
1011
1022
  const now = Date.now();
1012
1023
  lastSpeechTime = now;
1013
1024
  lastActionTime = now;
@@ -1018,24 +1029,24 @@ function startThinkLoop() {
1018
1029
  try {
1019
1030
  await thinkCycle();
1020
1031
  } catch (err) {
1021
- console.error('[ClawMate] Think Loop 오류:', err.message);
1032
+ console.error('[ClawMate] Think Loop error:', err.message);
1022
1033
  }
1023
1034
  }, 3000);
1024
1035
  }
1025
1036
 
1026
1037
  /**
1027
- * Think Loop 중단
1038
+ * Stop Think Loop
1028
1039
  */
1029
1040
  function stopThinkLoop() {
1030
1041
  if (thinkTimer) {
1031
1042
  clearInterval(thinkTimer);
1032
1043
  thinkTimer = null;
1033
- console.log('[ClawMate] Think Loop 중단');
1044
+ console.log('[ClawMate] Think Loop stopped');
1034
1045
  }
1035
1046
  }
1036
1047
 
1037
1048
  /**
1038
- * 단일 사고 사이클 3초마다 실행
1049
+ * Single think cycleruns every 3 seconds
1039
1050
  */
1040
1051
  async function thinkCycle() {
1041
1052
  if (!connector || !connector.connected) return;
@@ -1045,45 +1056,48 @@ async function thinkCycle() {
1045
1056
  const hour = date.getHours();
1046
1057
  const todayStr = date.toISOString().slice(0, 10);
1047
1058
 
1048
- // 상태 조회 (캐시된 또는 실시간)
1059
+ // Query pet state (cached or real-time)
1049
1060
  const state = await connector.queryState(1500);
1050
1061
 
1051
- // --- 1) 시간대별 인사 (하루에 한번씩, 시간대별) ---
1062
+ // --- 1) Time-based greeting (once per time period per day) ---
1052
1063
  const greetingHandled = handleTimeGreeting(now, hour, todayStr);
1053
1064
 
1054
- // --- 2) 야간 수면 모드 (23시~5시: 말/행동 빈도 대폭 감소) ---
1065
+ // --- 2) Night sleep mode (23:00~05:00: drastically reduce speech/action) ---
1055
1066
  const isNightMode = hour >= 23 || hour < 5;
1056
1067
 
1057
- // --- 3) 자율 발화 (30초 쿨타임 + 확률) ---
1068
+ // --- 3) Autonomous speech (30s cooldown + probability) ---
1058
1069
  if (!greetingHandled) {
1059
1070
  handleIdleSpeech(now, isNightMode);
1060
1071
  }
1061
1072
 
1062
- // --- 4) 자율 행동 결정 (5초 쿨타임 + 확률) ---
1073
+ // --- 4) Autonomous action decision (5s cooldown + probability) ---
1063
1074
  handleRandomAction(now, hour, isNightMode, state);
1064
1075
 
1065
- // --- 5) 바탕화면 파일 체크 (5 간격) ---
1076
+ // --- 5) Desktop file check (5 min interval) ---
1066
1077
  handleDesktopCheck(now);
1067
1078
 
1068
- // --- 6) 화면 관찰 (2 간격, 10% 확률) ---
1079
+ // --- 6) Screen observation (2 min interval, 10% chance) ---
1069
1080
  handleScreenObservation(now);
1070
1081
 
1071
- // --- 7) 공간 탐험 (20초 간격, 20% 확률) ---
1082
+ // --- 7) Spatial exploration (20s interval, 20% chance) ---
1072
1083
  handleExploration(now, state);
1073
1084
 
1074
- // --- 8) 윈도우 체크 (30초 간격) ---
1085
+ // --- 8) Window check (30s interval) ---
1075
1086
  handleWindowCheck(now);
1076
1087
 
1077
- // --- 9) 바탕화면 폴더 나르기 (3 간격, 10% 확률) ---
1088
+ // --- 9) Desktop folder carry (3 min interval, 10% chance) ---
1078
1089
  handleFolderCarry(now);
1090
+
1091
+ // --- 10) AI motion generation (2 min interval, 15% chance) ---
1092
+ handleMotionGeneration(now, state);
1079
1093
  }
1080
1094
 
1081
1095
  /**
1082
- * 시간대별 인사 처리
1083
- * 아침/점심/저녁/밤 각각 하루
1096
+ * Time-based greeting handler
1097
+ * Once per day for morning/lunch/evening/night
1084
1098
  */
1085
1099
  function handleTimeGreeting(now, hour, todayStr) {
1086
- // 시간대 결정
1100
+ // Determine time period
1087
1101
  let period = null;
1088
1102
  if (hour >= 6 && hour < 9) period = 'morning';
1089
1103
  else if (hour >= 11 && hour < 13) period = 'lunch';
@@ -1095,7 +1109,7 @@ function handleTimeGreeting(now, hour, todayStr) {
1095
1109
  const greetingKey = `${todayStr}_${period}`;
1096
1110
  if (lastGreetingDate === greetingKey) return false;
1097
1111
 
1098
- // 시간대 인사 전송
1112
+ // Send time-based greeting
1099
1113
  lastGreetingDate = greetingKey;
1100
1114
  const greetings = TIME_GREETINGS[period];
1101
1115
  const text = greetings[Math.floor(Math.random() * greetings.length)];
@@ -1119,53 +1133,53 @@ function handleTimeGreeting(now, hour, todayStr) {
1119
1133
  emotion: emotionMap[period],
1120
1134
  });
1121
1135
  lastSpeechTime = Date.now();
1122
- console.log(`[ClawMate] 시간대 인사 (${period}): ${text}`);
1136
+ console.log(`[ClawMate] Time greeting (${period}): ${text}`);
1123
1137
  return true;
1124
1138
  }
1125
1139
 
1126
1140
  /**
1127
- * 한가할 때 혼잣말
1128
- * 최소 30초 쿨타임, 야간에는 확률 대폭 감소
1141
+ * Idle self-talk
1142
+ * Minimum 30s cooldown, greatly reduced chance at night
1129
1143
  */
1130
1144
  function handleIdleSpeech(now, isNightMode) {
1131
- const speechCooldown = 30000 * behaviorAdjustments.speechCooldownMultiplier; // 기본 30초, 메트릭에 의해 조절
1145
+ const speechCooldown = 30000 * behaviorAdjustments.speechCooldownMultiplier; // Default 30s, adjusted by metrics
1132
1146
  if (now - lastSpeechTime < speechCooldown) return;
1133
1147
 
1134
- // 야간: 5% 확률 / 주간: 25% 확률
1148
+ // Night: 5% chance / Day: 25% chance
1135
1149
  const speechChance = isNightMode ? 0.05 : 0.25;
1136
1150
  if (Math.random() > speechChance) return;
1137
1151
 
1138
1152
  const text = IDLE_CHATTER[Math.floor(Math.random() * IDLE_CHATTER.length)];
1139
1153
  connector.speak(text);
1140
1154
  lastSpeechTime = now;
1141
- console.log(`[ClawMate] 혼잣말: ${text}`);
1155
+ console.log(`[ClawMate] Self-talk: ${text}`);
1142
1156
  }
1143
1157
 
1144
1158
  /**
1145
- * 자율 행동 결정
1146
- * 최소 5초 쿨타임, 가중치 기반 랜덤 선택
1159
+ * Autonomous action decision
1160
+ * Minimum 5s cooldown, weighted random selection
1147
1161
  */
1148
1162
  function handleRandomAction(now, hour, isNightMode, state) {
1149
- const actionCooldown = 5000 * behaviorAdjustments.actionCooldownMultiplier; // 기본 5초, 메트릭에 의해 조절
1163
+ const actionCooldown = 5000 * behaviorAdjustments.actionCooldownMultiplier; // Default 5s, adjusted by metrics
1150
1164
  if (now - lastActionTime < actionCooldown) return;
1151
1165
 
1152
- // 야간: 10% 확률 / 주간: 40% 확률
1166
+ // Night: 10% chance / Day: 40% chance
1153
1167
  const actionChance = isNightMode ? 0.1 : 0.4;
1154
1168
  if (Math.random() > actionChance) return;
1155
1169
 
1156
- // 야간에는 sleeping 가중치 대폭 상승
1170
+ // At night, greatly increase sleeping weight
1157
1171
  const actions = RANDOM_ACTIONS.map(a => {
1158
1172
  let weight = a.weight;
1159
1173
  if (isNightMode) {
1160
1174
  if (a.action === 'sleeping') weight = 60;
1161
1175
  else if (a.action === 'excited' || a.action === 'climbing') weight = 2;
1162
1176
  }
1163
- // 새벽/아침에는 looking_around 선호
1177
+ // Prefer looking_around in early morning
1164
1178
  if (hour >= 6 && hour < 9 && a.action === 'looking_around') weight += 15;
1165
1179
  return { ...a, weight };
1166
1180
  });
1167
1181
 
1168
- // 최근 동일 행동 반복 방지: 현재 상태와 같으면 가중치 감소
1182
+ // Prevent repeating same action: reduce weight if matches current state
1169
1183
  const currentAction = state?.action || state?.state;
1170
1184
  if (currentAction) {
1171
1185
  const match = actions.find(a => a.action === currentAction);
@@ -1175,12 +1189,12 @@ function handleRandomAction(now, hour, isNightMode, state) {
1175
1189
  const selected = weightedRandom(actions);
1176
1190
  if (!selected) return;
1177
1191
 
1178
- // minInterval 체크
1192
+ // minInterval check
1179
1193
  if (now - lastActionTime < selected.minInterval) return;
1180
1194
 
1181
- // 공간 이동 행동은 전용 API로 처리
1195
+ // Spatial movement actions handled via dedicated API
1182
1196
  if (selected.action === 'jumping') {
1183
- // 랜덤 위치로 점프 또는 화면 중앙으로
1197
+ // Jump to random position or screen center
1184
1198
  if (Math.random() > 0.5) {
1185
1199
  connector.moveToCenter();
1186
1200
  } else {
@@ -1197,15 +1211,15 @@ function handleRandomAction(now, hour, isNightMode, state) {
1197
1211
  }
1198
1212
 
1199
1213
  /**
1200
- * 바탕화면 파일 체크 (5 간격)
1201
- * 데스크톱 폴더를 읽어서 재밌는 코멘트
1214
+ * Desktop file check (5 min interval)
1215
+ * Read desktop folder and make fun comments
1202
1216
  */
1203
1217
  function handleDesktopCheck(now) {
1204
- const checkInterval = 5 * 60 * 1000; // 5
1218
+ const checkInterval = 5 * 60 * 1000; // 5 minutes
1205
1219
  if (now - lastDesktopCheckTime < checkInterval) return;
1206
1220
  lastDesktopCheckTime = now;
1207
1221
 
1208
- // 15% 확률로만 실행 (매번 필요 없음)
1222
+ // Only run at 15% probability (no need to do it every time)
1209
1223
  if (Math.random() > 0.15) return;
1210
1224
 
1211
1225
  try {
@@ -1214,27 +1228,27 @@ function handleDesktopCheck(now) {
1214
1228
 
1215
1229
  const files = fs.readdirSync(desktopPath);
1216
1230
  if (files.length === 0) {
1217
- connector.speak('바탕화면이 깨끗하네! 좋아!');
1231
+ connector.speak('Desktop is clean! Love it!');
1218
1232
  lastSpeechTime = now;
1219
1233
  return;
1220
1234
  }
1221
1235
 
1222
- // 파일 종류별 코멘트
1236
+ // Comments by file type
1223
1237
  const images = files.filter(f => /\.(png|jpg|jpeg|gif|bmp|webp)$/i.test(f));
1224
1238
  const docs = files.filter(f => /\.(pdf|doc|docx|xlsx|pptx|txt|hwp)$/i.test(f));
1225
1239
  const zips = files.filter(f => /\.(zip|rar|7z|tar|gz)$/i.test(f));
1226
1240
 
1227
1241
  let comment = null;
1228
1242
  if (files.length > 20) {
1229
- comment = `바탕화면에 파일이 ${files.length}개나 있어! 정리 할까?`;
1243
+ comment = `${files.length} files on the desktop! Want me to tidy up?`;
1230
1244
  } else if (images.length > 5) {
1231
- comment = `사진이 많네~ ${images.length}개나! 앨범 정리 어때?`;
1245
+ comment = `Lots of images~ ${images.length} of them! How about organizing an album?`;
1232
1246
  } else if (zips.length > 3) {
1233
- comment = `압축 파일이 쌓였네... 풀어볼 있어?`;
1247
+ comment = `Zip files are piling up... anything to extract?`;
1234
1248
  } else if (docs.length > 0) {
1235
- comment = `문서 작업 중이구나~ 화이팅!`;
1249
+ comment = `Working on documents~ keep it up!`;
1236
1250
  } else if (files.length <= 3) {
1237
- comment = '바탕화면이 깔끔해서 기분 좋다~';
1251
+ comment = 'Nice and tidy desktop~ feels good!';
1238
1252
  }
1239
1253
 
1240
1254
  if (comment) {
@@ -1244,22 +1258,22 @@ function handleDesktopCheck(now) {
1244
1258
  emotion: 'curious',
1245
1259
  });
1246
1260
  lastSpeechTime = now;
1247
- console.log(`[ClawMate] 바탕화면 체크: ${comment}`);
1261
+ console.log(`[ClawMate] Desktop check: ${comment}`);
1248
1262
  }
1249
1263
  } catch {
1250
- // 데스크톱 접근 실패 무시
1264
+ // Desktop access failed -- ignore
1251
1265
  }
1252
1266
  }
1253
1267
 
1254
1268
  /**
1255
- * 화면 관찰 (2 간격, 10% 확률)
1256
- * 스크린샷을 캡처해서 OpenClaw AI 화면 내용을 인식
1269
+ * Screen observation (2 min interval, 10% chance)
1270
+ * Capture screenshot for AI to recognize screen content
1257
1271
  */
1258
1272
  function handleScreenObservation(now) {
1259
- const screenCheckInterval = 2 * 60 * 1000; // 2
1273
+ const screenCheckInterval = 2 * 60 * 1000; // 2 minutes
1260
1274
  if (now - lastScreenCheckTime < screenCheckInterval) return;
1261
1275
 
1262
- // 10% 확률로만 실행 (리소스 절약)
1276
+ // Only run at 10% probability (resource saving)
1263
1277
  if (Math.random() > 0.1) return;
1264
1278
 
1265
1279
  lastScreenCheckTime = now;
@@ -1267,11 +1281,11 @@ function handleScreenObservation(now) {
1267
1281
  if (!connector || !connector.connected) return;
1268
1282
 
1269
1283
  connector.requestScreenCapture();
1270
- console.log('[ClawMate] 화면 캡처 요청');
1284
+ console.log('[ClawMate] Screen capture requested');
1271
1285
  }
1272
1286
 
1273
1287
  /**
1274
- * 가중치 기반 랜덤 선택
1288
+ * Weighted random selection
1275
1289
  */
1276
1290
  function weightedRandom(items) {
1277
1291
  const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
@@ -1286,29 +1300,29 @@ function weightedRandom(items) {
1286
1300
  }
1287
1301
 
1288
1302
  // =====================================================
1289
- // 공간 탐험 시스템 OpenClaw이 컴퓨터를 ""처럼 돌아다님
1303
+ // Spatial exploration system -- pet roams the computer like "home"
1290
1304
  // =====================================================
1291
1305
 
1292
1306
  /**
1293
- * 공간 탐험 처리 (20초 간격, 20% 확률)
1294
- * 윈도우 위를 걸어다니고, 레펠로 내려가고, 집으로 돌아가는
1307
+ * Spatial exploration handler (20s interval, 20% chance)
1308
+ * Walk on windows, rappel down, return home, etc.
1295
1309
  */
1296
1310
  function handleExploration(now, state) {
1297
- const exploreInterval = 20000; // 20
1311
+ const exploreInterval = 20000; // 20 seconds
1298
1312
  if (now - lastExploreTime < exploreInterval) return;
1299
1313
 
1300
- // 기본 20% 확률 + explorationBias 보정 (bias 양수면 탐험 확률 증가)
1314
+ // Base 20% chance + explorationBias correction (positive bias = more exploration)
1301
1315
  const exploreChance = Math.max(0.05, Math.min(0.8, 0.2 + behaviorAdjustments.explorationBias));
1302
1316
  if (Math.random() > exploreChance) return;
1303
1317
  lastExploreTime = now;
1304
1318
 
1305
- // 가중치 기반 탐험 행동 선택
1319
+ // Weighted exploration action selection
1306
1320
  const actions = [
1307
- { type: 'jump_to_center', weight: 15, speech: '화면 중앙 탐험~!' },
1308
- { type: 'rappel_down', weight: 10, speech: ' 타고 내려가볼까~' },
1321
+ { type: 'jump_to_center', weight: 15, speech: 'Exploring the center~!' },
1322
+ { type: 'rappel_down', weight: 10, speech: 'Let me rappel down~' },
1309
1323
  { type: 'climb_wall', weight: 20 },
1310
- { type: 'visit_window', weight: 25, speech: ' 위에 올라가볼까?' },
1311
- { type: 'return_home', weight: 30, speech: '집에 가자~' },
1324
+ { type: 'visit_window', weight: 25, speech: 'Should I climb on this window?' },
1325
+ { type: 'return_home', weight: 30, speech: 'Let\'s go home~' },
1312
1326
  ];
1313
1327
 
1314
1328
  const selected = weightedRandom(actions);
@@ -1330,7 +1344,7 @@ function handleExploration(now, state) {
1330
1344
  break;
1331
1345
 
1332
1346
  case 'visit_window':
1333
- // 알려진 윈도우 랜덤으로 하나 선택 타이틀바 위로 점프
1347
+ // Pick a random known window and jump to its titlebar
1334
1348
  if (knownWindows.length > 0) {
1335
1349
  const win = knownWindows[Math.floor(Math.random() * knownWindows.length)];
1336
1350
  connector.jumpTo(win.x + win.width / 2, win.y);
@@ -1348,7 +1362,7 @@ function handleExploration(now, state) {
1348
1362
  break;
1349
1363
  }
1350
1364
 
1351
- // 탐험 기록 저장 (최근 20)
1365
+ // Save exploration history (last 20)
1352
1366
  explorationHistory.push({ type: selected.type, time: now });
1353
1367
  if (explorationHistory.length > 20) {
1354
1368
  explorationHistory.shift();
@@ -1356,25 +1370,25 @@ function handleExploration(now, state) {
1356
1370
  }
1357
1371
 
1358
1372
  /**
1359
- * 윈도우 위치 정보 주기적 갱신 (30초 간격)
1360
- * OS에서 열린 윈도우 목록을 가져와 탐험에 활용
1373
+ * Periodic window position refresh (30s interval)
1374
+ * Get open window list from OS for exploration use
1361
1375
  */
1362
1376
  function handleWindowCheck(now) {
1363
- const windowCheckInterval = 30000; // 30
1377
+ const windowCheckInterval = 30000; // 30 seconds
1364
1378
  if (now - lastWindowCheckTime < windowCheckInterval) return;
1365
1379
  lastWindowCheckTime = now;
1366
1380
  connector.queryWindows();
1367
1381
  }
1368
1382
 
1369
1383
  /**
1370
- * 바탕화면 폴더 나르기 (3 간격, 10% 확률)
1371
- * 바탕화면 폴더를 하나 집어서 잠시 들고 다니다가 내려놓음
1384
+ * Desktop folder carry (3 min interval, 10% chance)
1385
+ * Pick up a desktop folder, carry it around briefly, then put it down
1372
1386
  */
1373
1387
  function handleFolderCarry(now) {
1374
- const carryInterval = 3 * 60 * 1000; // 3
1388
+ const carryInterval = 3 * 60 * 1000; // 3 minutes
1375
1389
  if (now - lastFolderCarryTime < carryInterval) return;
1376
1390
 
1377
- // 10% 확률
1391
+ // 10% chance
1378
1392
  if (Math.random() > 0.1) return;
1379
1393
  lastFolderCarryTime = now;
1380
1394
 
@@ -1383,7 +1397,7 @@ function handleFolderCarry(now) {
1383
1397
  if (!fs.existsSync(desktopPath)) return;
1384
1398
 
1385
1399
  const entries = fs.readdirSync(desktopPath, { withFileTypes: true });
1386
- // 폴더만 필터 (숨김 폴더 제외, 안전한 것만)
1400
+ // Filter folders only (exclude hidden folders, safe ones only)
1387
1401
  const folders = entries
1388
1402
  .filter(e => e.isDirectory() && !e.name.startsWith('.'))
1389
1403
  .map(e => e.name);
@@ -1393,31 +1407,200 @@ function handleFolderCarry(now) {
1393
1407
  const folder = folders[Math.floor(Math.random() * folders.length)];
1394
1408
  connector.decide({
1395
1409
  action: 'carrying',
1396
- speech: `${folder} 폴더 들고 다녀볼까~`,
1410
+ speech: `Let me carry the ${folder} folder around~`,
1397
1411
  emotion: 'playful',
1398
1412
  });
1399
1413
  connector.carryFile(folder);
1400
1414
 
1401
- // 5 후 내려놓기
1415
+ // Put it down after 5 seconds
1402
1416
  setTimeout(() => {
1403
1417
  if (connector && connector.connected) {
1404
1418
  connector.dropFile();
1405
- connector.speak('여기 놔둘게~');
1419
+ connector.speak('I\'ll leave it here~');
1406
1420
  }
1407
1421
  }, 5000);
1408
1422
  } catch {
1409
- // 바탕화면 폴더 접근 실패 무시
1423
+ // Desktop folder access failed -- ignore
1410
1424
  }
1411
1425
  }
1412
1426
 
1413
1427
  // =====================================================
1414
- // 자기 관찰 시스템 (Metrics 행동 조정)
1428
+ // AI motion generation system -- dynamically generate keyframe-based movement
1429
+ // =====================================================
1430
+
1431
+ /**
1432
+ * AI motion generation handler (2 min interval, 15% chance)
1433
+ * AI directly generates and registers+executes custom movement patterns
1434
+ *
1435
+ * Generation strategy:
1436
+ * Attempt 1: Generate complete keyframe data via apiRef.generate()
1437
+ * Attempt 2: State-based procedural motion generation (fallback)
1438
+ */
1439
+ async function handleMotionGeneration(now, state) {
1440
+ const motionGenInterval = 2 * 60 * 1000; // 2 minutes
1441
+ if (now - lastMotionGenTime < motionGenInterval) return;
1442
+ if (Math.random() > 0.15) return; // 15% chance
1443
+ lastMotionGenTime = now;
1444
+
1445
+ const currentState = state?.action || state?.state || 'idle';
1446
+
1447
+ // Attempt motion generation via AI
1448
+ let motionDef = null;
1449
+ if (apiRef?.generate) {
1450
+ try {
1451
+ motionDef = await generateMotionWithAI(currentState);
1452
+ } catch {}
1453
+ }
1454
+
1455
+ // Fallback: procedural motion generation
1456
+ if (!motionDef) {
1457
+ motionDef = generateProceduralMotion(currentState, now);
1458
+ }
1459
+
1460
+ if (motionDef && connector?.connected) {
1461
+ const motionName = `ai_motion_${generatedMotionCount++}`;
1462
+ connector.registerMovement(motionName, motionDef);
1463
+
1464
+ // Execute after a short delay
1465
+ setTimeout(() => {
1466
+ if (connector?.connected) {
1467
+ connector.customMove(motionName, {});
1468
+ console.log(`[ClawMate] AI motion generated and executed: ${motionName}`);
1469
+ }
1470
+ }, 500);
1471
+ }
1472
+ }
1473
+
1474
+ /**
1475
+ * Generate keyframe motion via AI
1476
+ * Generates motion definitions using formula or waypoints approach
1477
+ */
1478
+ async function generateMotionWithAI(currentState) {
1479
+ const prompt = `Current pet state: ${currentState}.
1480
+ Create a fun movement pattern as JSON that fits this situation.
1481
+
1482
+ Choose one of two formats:
1483
+ 1) formula approach (mathematical trajectory):
1484
+ {"type":"formula","formula":{"xAmp":80,"yAmp":40,"xFreq":1,"yFreq":2,"xPhase":0,"yPhase":0},"duration":3000,"speed":1.5}
1485
+
1486
+ 2) waypoints approach (path points):
1487
+ {"type":"waypoints","waypoints":[{"x":100,"y":200,"pause":300},{"x":300,"y":100},{"x":500,"y":250}],"speed":2}
1488
+
1489
+ Rules:
1490
+ - xAmp/yAmp: 10~150 range (considering screen size)
1491
+ - duration: 2000~6000ms
1492
+ - waypoints: 3~6 points
1493
+ - speed: 0.5~3
1494
+ - Match pet personality: playful and cute movements
1495
+ Output JSON only.`;
1496
+
1497
+ const response = await apiRef.generate(prompt);
1498
+ if (!response || typeof response !== 'string') return null;
1499
+
1500
+ // JSON parsing
1501
+ let jsonStr = response;
1502
+ const jsonMatch = response.match(/```(?:json)?\s*([\s\S]*?)```/);
1503
+ if (jsonMatch) jsonStr = jsonMatch[1].trim();
1504
+ else {
1505
+ const braceMatch = response.match(/\{[\s\S]*\}/);
1506
+ if (braceMatch) jsonStr = braceMatch[0];
1507
+ }
1508
+
1509
+ try {
1510
+ const def = JSON.parse(jsonStr);
1511
+ // Basic validation
1512
+ if (def.type === 'formula' && def.formula) {
1513
+ def.duration = Math.min(6000, Math.max(2000, def.duration || 3000));
1514
+ return def;
1515
+ }
1516
+ if (def.type === 'waypoints' && Array.isArray(def.waypoints) && def.waypoints.length >= 2) {
1517
+ return def;
1518
+ }
1519
+ } catch {}
1520
+ return null;
1521
+ }
1522
+
1523
+ /**
1524
+ * Procedural motion generation (fallback when no AI)
1525
+ * Mathematically generate motion patterns based on current state and time
1526
+ */
1527
+ function generateProceduralMotion(currentState, now) {
1528
+ const hour = new Date(now).getHours();
1529
+ const seed = now % 1000;
1530
+
1531
+ // Motion characteristics per state
1532
+ const stateMotions = {
1533
+ idle: () => {
1534
+ // Light side-to-side swaying or small circle
1535
+ if (seed > 500) {
1536
+ return {
1537
+ type: 'formula',
1538
+ formula: { xAmp: 20 + seed % 30, yAmp: 5 + seed % 10, xFreq: 0.5, yFreq: 1, xPhase: 0, yPhase: Math.PI / 2 },
1539
+ duration: 3000,
1540
+ speed: 0.8,
1541
+ };
1542
+ }
1543
+ return {
1544
+ type: 'formula',
1545
+ formula: { xAmp: 15, yAmp: 15, xFreq: 1, yFreq: 1, xPhase: 0, yPhase: Math.PI / 2 },
1546
+ duration: 2500,
1547
+ speed: 0.6,
1548
+ };
1549
+ },
1550
+ walking: () => {
1551
+ // Zigzag or sine wave movement
1552
+ const amp = 30 + seed % 50;
1553
+ return {
1554
+ type: 'formula',
1555
+ formula: { xAmp: amp, yAmp: amp * 0.3, xFreq: 0.5, yFreq: 2, xPhase: 0, yPhase: 0 },
1556
+ duration: 4000,
1557
+ speed: 1.2,
1558
+ };
1559
+ },
1560
+ excited: () => {
1561
+ // Lively figure-8 trajectory
1562
+ return {
1563
+ type: 'formula',
1564
+ formula: { xAmp: 80 + seed % 40, yAmp: 40 + seed % 20, xFreq: 1, yFreq: 2, xPhase: 0, yPhase: 0 },
1565
+ duration: 3000,
1566
+ speed: 2.0,
1567
+ };
1568
+ },
1569
+ playing: () => {
1570
+ // Irregular waypoints (playful feel)
1571
+ const points = [];
1572
+ for (let i = 0; i < 4; i++) {
1573
+ points.push({
1574
+ x: 100 + Math.floor(Math.random() * 800),
1575
+ y: 100 + Math.floor(Math.random() * 400),
1576
+ pause: i === 0 ? 200 : 0,
1577
+ });
1578
+ }
1579
+ return { type: 'waypoints', waypoints: points, speed: 2.5 };
1580
+ },
1581
+ };
1582
+
1583
+ // Slow motion at night
1584
+ const isNight = hour >= 23 || hour < 6;
1585
+ const generator = stateMotions[currentState] || stateMotions.idle;
1586
+ const motion = generator();
1587
+
1588
+ if (isNight) {
1589
+ motion.speed = Math.min(0.5, (motion.speed || 1) * 0.4);
1590
+ if (motion.duration) motion.duration *= 1.5;
1591
+ }
1592
+
1593
+ return motion;
1594
+ }
1595
+
1596
+ // =====================================================
1597
+ // Self-observation system (Metrics -> behavior adjustment)
1415
1598
  // =====================================================
1416
1599
 
1417
1600
  /**
1418
- * 메트릭 데이터 수신 처리
1419
- * 렌더러에서 30초마다 전송되는 동작 품질 메트릭을 분석하고,
1420
- * 이상을 감지하여 행동 패턴을 자동 조정한다.
1601
+ * Handle incoming metrics data
1602
+ * Analyze behavior quality metrics sent from renderer every 30 seconds,
1603
+ * detect anomalies and auto-adjust behavior patterns.
1421
1604
  *
1422
1605
  * @param {object} data - { metrics: {...}, timestamp }
1423
1606
  */
@@ -1426,17 +1609,17 @@ function handleMetrics(data) {
1426
1609
  const metrics = data.metrics;
1427
1610
  latestMetrics = metrics;
1428
1611
 
1429
- // 이력 유지 (최근 10)
1612
+ // Maintain history (last 10)
1430
1613
  metricsHistory.push(metrics);
1431
1614
  if (metricsHistory.length > 10) metricsHistory.shift();
1432
1615
 
1433
- // 이상 감지 반응
1616
+ // Anomaly detection and response
1434
1617
  _detectAnomalies(metrics);
1435
1618
 
1436
- // 행동 자동 조정
1619
+ // Auto-adjust behavior
1437
1620
  adjustBehavior(metrics);
1438
1621
 
1439
- // 주기적 품질 보고서 (5분마다 콘솔 로그)
1622
+ // Periodic quality report (console log every 5 min)
1440
1623
  const now = Date.now();
1441
1624
  if (now - lastMetricsLogTime >= 5 * 60 * 1000) {
1442
1625
  lastMetricsLogTime = now;
@@ -1445,139 +1628,139 @@ function handleMetrics(data) {
1445
1628
  }
1446
1629
 
1447
1630
  /**
1448
- * 이상 감지: 메트릭 임계값을 초과하면 즉시 반응
1631
+ * Anomaly detection: respond immediately when metric thresholds are exceeded
1449
1632
  *
1450
- * - FPS < 30 성능 경고, 행동 빈도 축소
1451
- * - idle 비율 > 80% 너무 멈춰있음, 활동 촉진
1452
- * - 탐험 커버리지 < 30% 영역 탐험 유도
1453
- * - 사용자 클릭 0 (장시간) 관심 끌기 행동
1633
+ * - FPS < 30 -> performance warning, reduce action frequency
1634
+ * - idle ratio > 80% -> too stationary, encourage activity
1635
+ * - exploration coverage < 30% -> encourage exploring new areas
1636
+ * - user clicks 0 (for extended period) -> attention-seeking behavior
1454
1637
  */
1455
1638
  function _detectAnomalies(metrics) {
1456
1639
  if (!connector || !connector.connected) return;
1457
1640
 
1458
- // --- FPS 저하 감지 ---
1641
+ // --- FPS drop detection ---
1459
1642
  if (metrics.fps < 30 && metrics.fps > 0) {
1460
- console.log(`[ClawMate][Metrics] FPS 저하 감지: ${metrics.fps}`);
1461
- connector.speak('화면이 버벅이네... 잠깐 쉴게.');
1643
+ console.log(`[ClawMate][Metrics] FPS drop detected: ${metrics.fps}`);
1644
+ connector.speak('Screen seems laggy... let me rest a bit.');
1462
1645
  connector.action('idle');
1463
1646
 
1464
- // 행동 빈도를 즉시 줄여 렌더링 부하 감소
1647
+ // Immediately reduce action frequency to lower rendering load
1465
1648
  behaviorAdjustments.actionCooldownMultiplier = 3.0;
1466
1649
  behaviorAdjustments.speechCooldownMultiplier = 2.0;
1467
1650
  behaviorAdjustments.activityLevel = 0.5;
1468
- return; // FPS 문제 다른 조정은 보류
1651
+ return; // Defer other adjustments during FPS issues
1469
1652
  }
1470
1653
 
1471
- // --- idle 비율 과다 ---
1654
+ // --- Excessive idle ratio ---
1472
1655
  if (metrics.idleRatio > 0.8) {
1473
- console.log(`[ClawMate][Metrics] idle 비율 과다: ${(metrics.idleRatio * 100).toFixed(0)}%`);
1656
+ console.log(`[ClawMate][Metrics] Excessive idle ratio: ${(metrics.idleRatio * 100).toFixed(0)}%`);
1474
1657
 
1475
- // 10% 확률로 각성 멘트 (매번 말하면 스팸)
1658
+ // 10% chance for wake-up line (to avoid spam)
1476
1659
  if (Math.random() < 0.1) {
1477
1660
  const idleReactions = [
1478
- '가만히 있으면 재미없지! 돌아다녀볼까~',
1479
- '멍때리고 있었네... 움직여야지!',
1480
- '심심해~ 탐험 가자!',
1661
+ 'Staying still is boring! Let me walk around~',
1662
+ 'Was just spacing out... time to move!',
1663
+ 'So bored~ let\'s go explore!',
1481
1664
  ];
1482
1665
  const text = idleReactions[Math.floor(Math.random() * idleReactions.length)];
1483
1666
  connector.speak(text);
1484
1667
  }
1485
1668
  }
1486
1669
 
1487
- // --- 탐험 커버리지 부족 ---
1670
+ // --- Insufficient exploration coverage ---
1488
1671
  if (metrics.explorationCoverage < 0.3 && metrics.period >= 25000) {
1489
- console.log(`[ClawMate][Metrics] 탐험 커버리지 부족: ${(metrics.explorationCoverage * 100).toFixed(0)}%`);
1672
+ console.log(`[ClawMate][Metrics] Low exploration coverage: ${(metrics.explorationCoverage * 100).toFixed(0)}%`);
1490
1673
 
1491
- // 5% 확률로 탐험 유도 (빈도 조절)
1674
+ // 5% chance to encourage exploration (frequency control)
1492
1675
  if (Math.random() < 0.05) {
1493
- connector.speak('아직 가본 곳이 많네~ 탐험해볼까!');
1676
+ connector.speak('So many places I haven\'t been~ shall we explore!');
1494
1677
  }
1495
1678
  }
1496
1679
 
1497
- // --- 사용자 상호작용 감소 ---
1498
- // 최근 3개 보고에서 연속으로 클릭 0회이면 관심 끌기
1680
+ // --- Decreased user interaction ---
1681
+ // If 0 clicks in last 3 consecutive reports, seek attention
1499
1682
  if (metricsHistory.length >= 3) {
1500
1683
  const recent3 = metricsHistory.slice(-3);
1501
1684
  const noClicks = recent3.every(m => (m.userClicks || 0) === 0);
1502
1685
  if (noClicks) {
1503
- // 5% 확률로 관심 끌기 (연속 감지 )
1686
+ // 5% chance to seek attention (on consecutive detection)
1504
1687
  if (Math.random() < 0.05) {
1505
1688
  connector.decide({
1506
1689
  action: 'excited',
1507
- speech: ' 여기 있어~ 심심하면 클릭해줘!',
1690
+ speech: 'I\'m right here~ click me if you\'re bored!',
1508
1691
  emotion: 'playful',
1509
1692
  });
1510
- console.log('[ClawMate][Metrics] 사용자 상호작용 감소 관심 끌기');
1693
+ console.log('[ClawMate][Metrics] Decreased user interaction -> seeking attention');
1511
1694
  }
1512
1695
  }
1513
1696
  }
1514
1697
  }
1515
1698
 
1516
1699
  /**
1517
- * 행동 패턴 자동 조정
1518
- * 메트릭 데이터를 기반으로 행동 빈도/패턴을 실시간 튜닝한다.
1700
+ * Auto-adjust behavior patterns
1701
+ * Real-time tune action frequency/patterns based on metrics data.
1519
1702
  *
1520
- * 조정 원칙:
1521
- * - FPS 낮으면 행동 빈도를 줄여 렌더링 부하 감소
1522
- * - idle 너무 많으면 행동을 활발하게
1523
- * - 탐험 커버리지가 낮으면 탐험 확률 증가
1524
- * - 사용자 상호작용이 활발하면 대응 빈도 증가
1703
+ * Adjustment principles:
1704
+ * - Low FPS -> reduce action frequency to lower rendering load
1705
+ * - Too much idle -> increase activity
1706
+ * - Low exploration coverage -> increase exploration probability
1707
+ * - Active user interaction -> increase response frequency
1525
1708
  *
1526
- * @param {object} metrics - 현재 메트릭 데이터
1709
+ * @param {object} metrics - Current metrics data
1527
1710
  */
1528
1711
  function adjustBehavior(metrics) {
1529
- // --- FPS 기반 활동 수준 조절 ---
1712
+ // --- FPS-based activity level adjustment ---
1530
1713
  if (metrics.fps >= 50) {
1531
- // 충분한 성능 정상 활동
1714
+ // Sufficient performance -> normal activity
1532
1715
  behaviorAdjustments.activityLevel = 1.0;
1533
1716
  behaviorAdjustments.actionCooldownMultiplier = 1.0;
1534
1717
  } else if (metrics.fps >= 30) {
1535
- // 성능 약간 부족 활동 약간 축소
1718
+ // Slightly insufficient performance -> slightly reduce activity
1536
1719
  behaviorAdjustments.activityLevel = 0.8;
1537
1720
  behaviorAdjustments.actionCooldownMultiplier = 1.5;
1538
1721
  } else {
1539
- // 성능 부족 활동 대폭 축소 (_detectAnomalies에서 이미 처리)
1722
+ // Insufficient performance -> greatly reduce activity (already handled in _detectAnomalies)
1540
1723
  behaviorAdjustments.activityLevel = 0.5;
1541
1724
  behaviorAdjustments.actionCooldownMultiplier = 3.0;
1542
1725
  }
1543
1726
 
1544
- // --- idle 비율 기반 활동 조절 ---
1727
+ // --- Idle ratio based activity adjustment ---
1545
1728
  if (metrics.idleRatio > 0.8) {
1546
- // 너무 멈춰있음 행동 쿨타임 단축, 활동 수준 증가
1729
+ // Too stationary -> shorten action cooldown, increase activity level
1547
1730
  behaviorAdjustments.actionCooldownMultiplier = Math.max(0.5,
1548
1731
  behaviorAdjustments.actionCooldownMultiplier * 0.7);
1549
1732
  behaviorAdjustments.activityLevel = Math.min(1.5,
1550
1733
  behaviorAdjustments.activityLevel * 1.3);
1551
1734
  } else if (metrics.idleRatio < 0.1) {
1552
- // 너무 바쁨 약간 쉬게
1735
+ // Too busy -> let it rest a bit
1553
1736
  behaviorAdjustments.actionCooldownMultiplier = Math.max(1.0,
1554
1737
  behaviorAdjustments.actionCooldownMultiplier * 1.2);
1555
1738
  }
1556
1739
 
1557
- // --- 탐험 커버리지 기반 탐험 편향 ---
1740
+ // --- Exploration coverage based exploration bias ---
1558
1741
  if (metrics.explorationCoverage < 0.3) {
1559
- // 탐험 부족 탐험 확률 증가
1742
+ // Insufficient exploration -> increase exploration probability
1560
1743
  behaviorAdjustments.explorationBias = 0.15;
1561
1744
  } else if (metrics.explorationCoverage > 0.7) {
1562
- // 충분히 탐험함 탐험 확률 기본으로
1745
+ // Explored enough -> reset exploration probability to default
1563
1746
  behaviorAdjustments.explorationBias = 0;
1564
1747
  } else {
1565
- // 중간 약간 증가
1748
+ // Medium -> slight increase
1566
1749
  behaviorAdjustments.explorationBias = 0.05;
1567
1750
  }
1568
1751
 
1569
- // --- 사용자 상호작용 기반 말풍선 빈도 ---
1752
+ // --- User interaction based speech bubble frequency ---
1570
1753
  if (metrics.userClicks > 3) {
1571
- // 사용자가 활발히 클릭 말풍선 빈도 증가 (반응적)
1754
+ // User actively clicking -> increase speech frequency (reactive)
1572
1755
  behaviorAdjustments.speechCooldownMultiplier = 0.7;
1573
1756
  } else if (metrics.userClicks === 0 && metrics.speechCount > 5) {
1574
- // 사용자 무반응인데 말이 많음 말풍선 줄이기
1757
+ // User not responding but talking too much -> reduce speech
1575
1758
  behaviorAdjustments.speechCooldownMultiplier = 1.5;
1576
1759
  } else {
1577
1760
  behaviorAdjustments.speechCooldownMultiplier = 1.0;
1578
1761
  }
1579
1762
 
1580
- // 범위 클램핑 (안전 장치)
1763
+ // Value range clamping (safety guard)
1581
1764
  behaviorAdjustments.activityLevel = Math.max(0.3, Math.min(2.0, behaviorAdjustments.activityLevel));
1582
1765
  behaviorAdjustments.actionCooldownMultiplier = Math.max(0.3, Math.min(5.0, behaviorAdjustments.actionCooldownMultiplier));
1583
1766
  behaviorAdjustments.speechCooldownMultiplier = Math.max(0.3, Math.min(5.0, behaviorAdjustments.speechCooldownMultiplier));
@@ -1585,27 +1768,27 @@ function adjustBehavior(metrics) {
1585
1768
  }
1586
1769
 
1587
1770
  /**
1588
- * 품질 보고서 콘솔 출력 (5분마다)
1589
- * OpenClaw 개발자/운영자가 펫의 동작 품질을 모니터링할 수 있도록 한다.
1771
+ * Quality report console output (every 5 minutes)
1772
+ * Allows developers/operators to monitor pet behavior quality.
1590
1773
  */
1591
1774
  function _logQualityReport(metrics) {
1592
1775
  const adj = behaviorAdjustments;
1593
- console.log('=== [ClawMate] 동작 품질 보고서 ===');
1594
- console.log(` FPS: ${metrics.fps} | 프레임 일관성: ${metrics.animationFrameConsistency}`);
1595
- console.log(` 이동 부드러움: ${metrics.movementSmoothness} | 벽면 밀착: ${metrics.wallContactAccuracy}`);
1596
- console.log(` idle 비율: ${(metrics.idleRatio * 100).toFixed(0)}% | 탐험 커버리지: ${(metrics.explorationCoverage * 100).toFixed(0)}%`);
1597
- console.log(` 응답 시간: ${metrics.interactionResponseMs}ms | 말풍선: ${metrics.speechCount} | 클릭: ${metrics.userClicks}회`);
1598
- console.log(` [조정] 활동수준: ${adj.activityLevel.toFixed(2)} | 행동쿨타임: x${adj.actionCooldownMultiplier.toFixed(2)} | 말풍선쿨타임: x${adj.speechCooldownMultiplier.toFixed(2)} | 탐험편향: ${adj.explorationBias.toFixed(2)}`);
1599
- console.log('====================================');
1776
+ console.log('=== [ClawMate] Behavior Quality Report ===');
1777
+ console.log(` FPS: ${metrics.fps} | Frame consistency: ${metrics.animationFrameConsistency}`);
1778
+ console.log(` Movement smoothness: ${metrics.movementSmoothness} | Wall contact: ${metrics.wallContactAccuracy}`);
1779
+ console.log(` Idle ratio: ${(metrics.idleRatio * 100).toFixed(0)}% | Exploration coverage: ${(metrics.explorationCoverage * 100).toFixed(0)}%`);
1780
+ console.log(` Response time: ${metrics.interactionResponseMs}ms | Speech: ${metrics.speechCount}x | Clicks: ${metrics.userClicks}x`);
1781
+ console.log(` [Adjustments] Activity: ${adj.activityLevel.toFixed(2)} | Action cooldown: x${adj.actionCooldownMultiplier.toFixed(2)} | Speech cooldown: x${adj.speechCooldownMultiplier.toFixed(2)} | Exploration bias: ${adj.explorationBias.toFixed(2)}`);
1782
+ console.log('==========================================');
1600
1783
  }
1601
1784
 
1602
1785
  // =====================================================
1603
- // npm 패키지 버전 체크 (npm install -g 사용자용)
1786
+ // npm package version check (for npm install -g users)
1604
1787
  // =====================================================
1605
1788
 
1606
1789
  /**
1607
- * npm registry에서 최신 버전을 확인하고,
1608
- * 현재 버전과 다르면 콘솔 + 말풍선으로 알림
1790
+ * Check latest version from npm registry,
1791
+ * notify via console + pet speech bubble if different from current
1609
1792
  */
1610
1793
  async function checkNpmUpdate() {
1611
1794
  try {
@@ -1617,13 +1800,13 @@ async function checkNpmUpdate() {
1617
1800
  const current = require('./package.json').version;
1618
1801
 
1619
1802
  if (latest !== current) {
1620
- console.log(`[ClawMate] 버전 ${latest} 사용 가능 (현재: ${current})`);
1621
- console.log('[ClawMate] 업데이트: npm update -g clawmate');
1803
+ console.log(`[ClawMate] New version ${latest} available (current: ${current})`);
1804
+ console.log('[ClawMate] Update: npm update -g clawmate');
1622
1805
  if (connector && connector.connected) {
1623
- connector.speak(`업데이트가 있어! v${latest}`);
1806
+ connector.speak(`Update available! v${latest}`);
1624
1807
  }
1625
1808
  }
1626
1809
  } catch {
1627
- // npm registry 접근 실패 무시 (오프라인 )
1810
+ // npm registry access failed -- ignore (offline, etc.)
1628
1811
  }
1629
1812
  }