clawmate 1.4.0 → 1.4.2

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