clawmate 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/main/tray.js CHANGED
@@ -1,5 +1,6 @@
1
- const { Tray, Menu, nativeImage, app } = require('electron');
1
+ const { Tray, Menu, nativeImage, app, shell } = require('electron');
2
2
  const path = require('path');
3
+ const { execSync } = require('child_process');
3
4
  const Store = require('./store');
4
5
  const { undoAllMoves, getFileManifest } = require('./file-ops');
5
6
  const { isAutoStartEnabled, toggleAutoStart } = require('./autostart');
@@ -7,28 +8,76 @@ const { isAutoStartEnabled, toggleAutoStart } = require('./autostart');
7
8
  let tray = null;
8
9
  let aiBridge = null;
9
10
 
11
+ /**
12
+ * 16x16 Claw 픽셀아트 아이콘 생성
13
+ * 캐릭터 idle 프레임을 축소한 형태
14
+ *
15
+ * 색상 코드:
16
+ * 0 = 투명
17
+ * 1 = #ff4f40 (빨강)
18
+ * 2 = #ff775f (연빨강)
19
+ * 3 = #3a0a0d (갈색 다리)
20
+ * 4 = #ffffff (눈 흰자)
21
+ * 5 = #000000 (눈동자)
22
+ * 6 = #ff4f40 (집게)
23
+ */
24
+ const CLAW_ICON = [
25
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
26
+ [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
27
+ [0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
28
+ [0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
29
+ [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
30
+ [0,0,0,6,1,1,0,0,0,0,1,1,6,0,0,0],
31
+ [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
32
+ [0,0,0,1,1,4,5,1,1,4,5,1,1,0,0,0],
33
+ [0,0,0,1,1,4,5,1,1,4,5,1,1,0,0,0],
34
+ [0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0],
35
+ [0,0,1,2,1,1,1,1,1,1,1,1,2,1,0,0],
36
+ [0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0],
37
+ [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
38
+ [0,0,3,3,3,0,3,3,3,3,0,3,3,3,0,0],
39
+ [0,3,3,3,0,0,0,3,3,0,0,0,3,3,3,0],
40
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
41
+ ];
42
+
43
+ const COLOR_MAP = {
44
+ 0: [0, 0, 0, 0], // 투명
45
+ 1: [255, 79, 64, 255], // primary 빨강
46
+ 2: [255, 119, 95, 255], // secondary 연빨강
47
+ 3: [58, 10, 13, 255], // dark 갈색
48
+ 4: [255, 255, 255, 255], // eye 흰자
49
+ 5: [0, 0, 0, 255], // pupil 눈동자
50
+ 6: [255, 79, 64, 255], // claw 집게
51
+ };
52
+
53
+ /**
54
+ * CLAW_ICON 16x16 배열 → nativeImage 변환
55
+ */
56
+ function createClawIcon() {
57
+ const size = 16;
58
+ const buffer = Buffer.alloc(size * size * 4);
59
+
60
+ for (let y = 0; y < size; y++) {
61
+ for (let x = 0; x < size; x++) {
62
+ const code = CLAW_ICON[y][x];
63
+ const color = COLOR_MAP[code] || COLOR_MAP[0];
64
+ const offset = (y * size + x) * 4;
65
+ buffer[offset + 0] = color[0]; // R
66
+ buffer[offset + 1] = color[1]; // G
67
+ buffer[offset + 2] = color[2]; // B
68
+ buffer[offset + 3] = color[3]; // A
69
+ }
70
+ }
71
+
72
+ return nativeImage.createFromBuffer(buffer, { width: size, height: size });
73
+ }
74
+
10
75
  function setupTray(mainWindow, bridge) {
11
76
  aiBridge = bridge;
12
77
  const store = new Store('clawmate-config', { mode: 'pet' });
13
78
 
14
- // 트레이 아이콘 생성
15
- let icon;
16
- try {
17
- const iconPath = path.join(__dirname, '..', 'assets', 'icons', 'tray-pet.png');
18
- icon = nativeImage.createFromPath(iconPath);
19
- if (icon.isEmpty()) throw new Error('no icon');
20
- } catch {
21
- // 16x16 빨간색 아이콘 폴백
22
- const size = 16;
23
- const buffer = Buffer.alloc(size * size * 4);
24
- for (let i = 0; i < size * size; i++) {
25
- buffer[i * 4 + 0] = 0xff;
26
- buffer[i * 4 + 1] = 0x4f;
27
- buffer[i * 4 + 2] = 0x40;
28
- buffer[i * 4 + 3] = 0xff;
29
- }
30
- icon = nativeImage.createFromBuffer(buffer, { width: size, height: size });
31
- }
79
+ // Claw 픽셀아트 트레이 아이콘 생성
80
+ const icon = createClawIcon();
32
81
 
33
82
  tray = new Tray(icon);
34
83
  tray.setToolTip('ClawMate - 데스크톱 펫');
@@ -89,6 +138,12 @@ function setupTray(mainWindow, bridge) {
89
138
  },
90
139
  },
91
140
  { type: 'separator' },
141
+ {
142
+ label: '업데이트 확인',
143
+ click: async () => {
144
+ await checkForUpdateManual(mainWindow);
145
+ },
146
+ },
92
147
  {
93
148
  label: '파일 이동 되돌리기',
94
149
  click: async () => {
@@ -122,4 +177,62 @@ function setupTray(mainWindow, bridge) {
122
177
  return tray;
123
178
  }
124
179
 
180
+ /**
181
+ * 수동 업데이트 확인 (트레이 메뉴에서 클릭)
182
+ * 빌드된 앱: electron-updater 사용
183
+ * npm 설치: npm registry에서 최신 버전 비교
184
+ */
185
+ async function checkForUpdateManual(mainWindow) {
186
+ if (app.isPackaged) {
187
+ // electron-updater 기반 업데이트
188
+ try {
189
+ const { autoUpdater } = require('electron-updater');
190
+ autoUpdater.checkForUpdatesAndNotify();
191
+ } catch (err) {
192
+ console.error('[업데이트] electron-updater 실패:', err.message);
193
+ }
194
+ } else {
195
+ // npm 기반 업데이트 확인
196
+ try {
197
+ const latest = execSync('npm view clawmate version', {
198
+ encoding: 'utf-8',
199
+ timeout: 10000,
200
+ }).trim();
201
+ const current = require('../package.json').version;
202
+
203
+ if (latest !== current) {
204
+ // 펫 말풍선으로 알림
205
+ if (mainWindow && !mainWindow.isDestroyed()) {
206
+ mainWindow.webContents.send('ai-command', {
207
+ type: 'speak',
208
+ payload: { text: `새 버전 v${latest} 사용 가능! (현재: v${current})` },
209
+ });
210
+ }
211
+ console.log(`[업데이트] 새 버전 ${latest} 사용 가능 (현재: ${current})`);
212
+ console.log('[업데이트] npm update -g clawmate');
213
+
214
+ // npm 페이지 열기
215
+ shell.openExternal('https://www.npmjs.com/package/clawmate');
216
+ } else {
217
+ // 이미 최신
218
+ if (mainWindow && !mainWindow.isDestroyed()) {
219
+ mainWindow.webContents.send('ai-command', {
220
+ type: 'speak',
221
+ payload: { text: `v${current} — 이미 최신 버전이야!` },
222
+ });
223
+ }
224
+ console.log(`[업데이트] 현재 최신 버전 (v${current})`);
225
+ }
226
+ } catch (err) {
227
+ console.error('[업데이트] npm 버전 확인 실패:', err.message);
228
+ if (mainWindow && !mainWindow.isDestroyed()) {
229
+ mainWindow.webContents.send('ai-command', {
230
+ type: 'speak',
231
+ payload: { text: '업데이트 확인 실패... 인터넷 연결 확인해봐!' },
232
+ });
233
+ }
234
+ }
235
+ }
236
+ }
237
+
125
238
  module.exports = { setupTray };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmate",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "OpenClaw 데스크톱 펫 - AI가 조종하는 화면 위의 살아있는 Claw",
5
5
  "main": "main/index.js",
6
6
  "bin": {
@@ -29,6 +29,7 @@
29
29
  "homepage": "https://github.com/boqum/clawmate",
30
30
  "dependencies": {
31
31
  "electron-updater": "^6.0.0",
32
+ "node-telegram-bot-api": "^0.66.0",
32
33
  "ws": "^8.18.0"
33
34
  },
34
35
  "devDependencies": {
@@ -62,4 +62,20 @@ contextBridge.exposeInMainWorld('clawmate', {
62
62
 
63
63
  // AI 연결 상태 확인
64
64
  isAIConnected: () => ipcRenderer.invoke('is-ai-connected'),
65
+
66
+ // 메트릭 보고 (렌더러 → main → OpenClaw)
67
+ reportMetrics: (summary) => ipcRenderer.send('report-metrics', summary),
68
+
69
+ // 활성 윈도우 제목 조회 (브라우저 감시)
70
+ getActiveWindowTitle: () => ipcRenderer.invoke('get-active-window-title'),
71
+
72
+ // 커서 위치 조회 (화면 좌표)
73
+ getCursorPosition: () => ipcRenderer.invoke('get-cursor-position'),
74
+
75
+ // === 스마트 파일 조작 ===
76
+ parseFileCommand: (text) => ipcRenderer.invoke('parse-file-command', text),
77
+ listFilteredFiles: (sourceDir, filter) => ipcRenderer.invoke('list-filtered-files', sourceDir, filter),
78
+ smartFileOp: (command) => ipcRenderer.invoke('smart-file-op', command),
79
+ undoSmartMove: (moveId) => ipcRenderer.invoke('undo-smart-move', moveId),
80
+ undoAllSmartMoves: () => ipcRenderer.invoke('undo-all-smart-moves'),
65
81
  });
@@ -60,6 +60,8 @@
60
60
  <script src="js/time-aware.js"></script>
61
61
  <script src="js/mode-manager.js"></script>
62
62
  <script src="js/memory.js"></script>
63
+ <script src="js/metrics.js"></script>
64
+ <script src="js/browser-watcher.js"></script>
63
65
  <script src="js/ai-controller.js"></script>
64
66
  <script src="js/app.js"></script>
65
67
  </body>
@@ -140,9 +140,303 @@ const AIController = (() => {
140
140
  // payload: { windowId, x, y }
141
141
  PetEngine.jumpTo(payload.x, payload.y);
142
142
  break;
143
+
144
+ // === 커스텀 이동 패턴 ===
145
+
146
+ case 'register_movement':
147
+ // OpenClaw이 JSON으로 이동 패턴 정의를 보내면 등록
148
+ // payload: { name, definition }
149
+ // definition: { type, params } — 각 타입별 파라미터
150
+ _registerAIMovement(payload.name, payload.definition);
151
+ break;
152
+
153
+ case 'custom_move':
154
+ // 등록된 커스텀 이동 패턴 실행
155
+ // payload: { name, params? }
156
+ if (!PetEngine.executeCustomMovement(payload.name, payload.params || {})) {
157
+ // 실행 실패 시 AI에 알림
158
+ if (window.clawmate.reportToAI) {
159
+ window.clawmate.reportToAI('custom_move_failed', {
160
+ name: payload.name,
161
+ available: PetEngine.getRegisteredMovements(),
162
+ });
163
+ }
164
+ }
165
+ break;
166
+
167
+ case 'stop_custom_move':
168
+ // 현재 커스텀 이동 강제 중지
169
+ PetEngine.stopCustomMovement();
170
+ break;
171
+
172
+ case 'list_movements':
173
+ // 등록된 이동 패턴 목록 요청
174
+ if (window.clawmate.reportToAI) {
175
+ window.clawmate.reportToAI('movement_list', {
176
+ movements: PetEngine.getRegisteredMovements(),
177
+ });
178
+ }
179
+ break;
180
+
181
+ // === 캐릭터 커스터마이징 ===
182
+ case 'set_character':
183
+ // AI가 생성한 새 캐릭터 데이터 적용
184
+ Character.setCharacterData(payload);
185
+ if (payload.speech) {
186
+ Speech.show(payload.speech);
187
+ } else {
188
+ Speech.show('변신 완료!');
189
+ }
190
+ StateMachine.forceState('excited');
191
+ setTimeout(() => {
192
+ if (StateMachine.getState() === 'excited') StateMachine.forceState('idle');
193
+ }, 2000);
194
+ break;
195
+
196
+ case 'reset_character':
197
+ // 원래 캐릭터로 복원
198
+ Character.resetCharacter();
199
+ Speech.show('원래 모습으로 돌아왔어!');
200
+ StateMachine.forceState('excited');
201
+ break;
202
+
203
+ // === 스마트 파일 조작 애니메이션 ===
204
+ case 'smart_file_op':
205
+ handleSmartFileOp(payload);
206
+ break;
143
207
  }
144
208
  }
145
209
 
210
+ /**
211
+ * 스마트 파일 조작 애니메이션 처리
212
+ *
213
+ * 텔레그램이나 AI에서 트리거된 파일 이동 작업의
214
+ * 각 단계(phase)에 따라 펫 애니메이션을 순차 실행.
215
+ *
216
+ * phase:
217
+ * - start: 작업 시작, 총 파일 수 표시
218
+ * - pick_up: 파일 집어들기 (carrying 상태 + 말풍선)
219
+ * - drop: 파일 내려놓기 (걷기 상태 + 말풍선)
220
+ * - complete: 완료 (excited 상태 + 결과 말풍선)
221
+ * - error: 오류 (scared 상태 + 에러 말풍선)
222
+ */
223
+ function handleSmartFileOp(payload) {
224
+ switch (payload.phase) {
225
+ case 'start':
226
+ StateMachine.forceState('excited');
227
+ Speech.show(`${payload.totalFiles}개 파일 정리 시작!`);
228
+ break;
229
+
230
+ case 'pick_up':
231
+ // 펫이 파일 위치로 이동 (화면 내 랜덤 위치)
232
+ _smartFileJumpToSource(payload.index);
233
+ // 집어들기 애니메이션
234
+ setTimeout(() => {
235
+ StateMachine.forceState('carrying');
236
+ Speech.show(`${payload.fileName} 집었다!`);
237
+ }, 400);
238
+ break;
239
+
240
+ case 'drop':
241
+ // 대상 폴더 위치로 이동
242
+ _smartFileJumpToTarget(payload.index);
243
+ // 내려놓기 애니메이션
244
+ setTimeout(() => {
245
+ StateMachine.forceState('walking');
246
+ Speech.show(`여기! (${payload.targetName})`);
247
+ }, 400);
248
+ break;
249
+
250
+ case 'complete':
251
+ StateMachine.forceState('excited');
252
+ if (payload.movedCount > 0) {
253
+ Speech.show(`${payload.movedCount}개 파일 옮겼어!`);
254
+ } else {
255
+ Speech.show('옮길 파일이 없었어!');
256
+ }
257
+ break;
258
+
259
+ case 'error':
260
+ StateMachine.forceState('scared');
261
+ Speech.show('앗, 뭔가 잘못됐어...');
262
+ break;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * 파일 집어들기 위치로 점프
268
+ * 파일 인덱스에 따라 화면 좌측 영역의 다른 위치로 이동
269
+ */
270
+ function _smartFileJumpToSource(index) {
271
+ const screenW = window.innerWidth;
272
+ const screenH = window.innerHeight;
273
+ // 화면 왼쪽 1/3 영역에서 세로 위치를 파일 인덱스에 따라 분산
274
+ const targetX = screenW * 0.1 + (index % 3) * 50;
275
+ const targetY = screenH * 0.3 + ((index * 80) % (screenH * 0.5));
276
+ PetEngine.jumpTo(targetX, targetY);
277
+ }
278
+
279
+ /**
280
+ * 파일 내려놓기 위치로 점프
281
+ * 화면 오른쪽 영역으로 이동
282
+ */
283
+ function _smartFileJumpToTarget(index) {
284
+ const screenW = window.innerWidth;
285
+ const screenH = window.innerHeight;
286
+ // 화면 오른쪽 1/3 영역
287
+ const targetX = screenW * 0.7 + (index % 3) * 50;
288
+ const targetY = screenH * 0.4 + ((index * 60) % (screenH * 0.4));
289
+ PetEngine.jumpTo(targetX, targetY);
290
+ }
291
+
292
+ /**
293
+ * OpenClaw AI가 JSON으로 정의한 이동 패턴을 동적으로 등록
294
+ * 안전한 실행을 위해 Function 생성자 대신 사전정의된 행동 유형 조합 사용
295
+ *
296
+ * definition 형식:
297
+ * {
298
+ * type: 'waypoints' | 'formula' | 'sequence',
299
+ * waypoints?: [{x, y, pause?}], // waypoints 타입
300
+ * formula?: { xExpr, yExpr }, // formula 타입 (sin, cos 기반)
301
+ * sequence?: ['zigzag', 'shake', ...], // sequence 타입 (기존 패턴 순차 실행)
302
+ * duration?: number,
303
+ * speed?: number,
304
+ * }
305
+ */
306
+ function _registerAIMovement(name, definition) {
307
+ if (!name || !definition || !definition.type) {
308
+ console.warn('[AIController] 이동 패턴 등록 실패: name, definition.type 필수');
309
+ return;
310
+ }
311
+
312
+ let handler;
313
+
314
+ switch (definition.type) {
315
+ // 웨이포인트 타입: 지정된 좌표들을 순서대로 이동
316
+ case 'waypoints':
317
+ handler = {
318
+ init(params) {
319
+ return {
320
+ waypoints: definition.waypoints || [],
321
+ currentIdx: 0,
322
+ speed: definition.speed || 2,
323
+ pauseTime: 0,
324
+ pausing: false,
325
+ };
326
+ },
327
+ update(dt, state, ctx) {
328
+ if (state.currentIdx >= state.waypoints.length) return;
329
+
330
+ const wp = state.waypoints[state.currentIdx];
331
+
332
+ // 웨이포인트에서 멈춤 중
333
+ if (state.pausing) {
334
+ state.pauseTime -= dt;
335
+ if (state.pauseTime <= 0) {
336
+ state.pausing = false;
337
+ state.currentIdx++;
338
+ }
339
+ return;
340
+ }
341
+
342
+ const dx = wp.x - ctx.x;
343
+ const dy = wp.y - ctx.y;
344
+ const dist = Math.hypot(dx, dy);
345
+
346
+ if (dist < 5) {
347
+ // 웨이포인트 도달
348
+ if (wp.pause && wp.pause > 0) {
349
+ state.pausing = true;
350
+ state.pauseTime = wp.pause;
351
+ } else {
352
+ state.currentIdx++;
353
+ }
354
+ return;
355
+ }
356
+
357
+ const step = state.speed * (dt / 16);
358
+ const ratio = Math.min(1, step / dist);
359
+ ctx.setPos(ctx.x + dx * ratio, ctx.y + dy * ratio);
360
+ ctx.setFlip(dx < 0);
361
+ },
362
+ isComplete(state) {
363
+ return state.currentIdx >= (state.waypoints || []).length;
364
+ },
365
+ cleanup() {},
366
+ };
367
+ break;
368
+
369
+ // 수식 타입: sin/cos 기반 수학적 궤도
370
+ case 'formula':
371
+ handler = {
372
+ init(params) {
373
+ return {
374
+ duration: definition.duration || 3000,
375
+ elapsed: 0,
376
+ originX: params.x,
377
+ originY: params.y,
378
+ xAmp: definition.formula?.xAmp || 50,
379
+ yAmp: definition.formula?.yAmp || 30,
380
+ xFreq: definition.formula?.xFreq || 1,
381
+ yFreq: definition.formula?.yFreq || 1,
382
+ xPhase: definition.formula?.xPhase || 0,
383
+ yPhase: definition.formula?.yPhase || 0,
384
+ };
385
+ },
386
+ update(dt, state, ctx) {
387
+ state.elapsed += dt;
388
+ const t = (state.elapsed / state.duration) * Math.PI * 2;
389
+ const nx = state.originX + Math.sin(t * state.xFreq + state.xPhase) * state.xAmp;
390
+ const ny = state.originY + Math.sin(t * state.yFreq + state.yPhase) * state.yAmp;
391
+ ctx.setPos(nx, ny);
392
+ ctx.setFlip(Math.cos(t * state.xFreq + state.xPhase) < 0);
393
+ },
394
+ isComplete(state) {
395
+ return state.elapsed >= state.duration;
396
+ },
397
+ cleanup() {},
398
+ };
399
+ break;
400
+
401
+ // 시퀀스 타입: 기존 등록된 패턴들을 순차 실행
402
+ case 'sequence':
403
+ handler = {
404
+ init(params) {
405
+ return {
406
+ sequence: definition.sequence || [],
407
+ currentIdx: 0,
408
+ subStarted: false,
409
+ };
410
+ },
411
+ update(dt, state, ctx) {
412
+ if (state.currentIdx >= state.sequence.length) return;
413
+
414
+ if (!state.subStarted) {
415
+ const subName = state.sequence[state.currentIdx];
416
+ // 서브 패턴을 직접 실행하지 않고 상태만 추적
417
+ PetEngine.executeCustomMovement(subName, {
418
+ x: ctx.x, y: ctx.y,
419
+ screenW: ctx.screenW, screenH: ctx.screenH,
420
+ });
421
+ state.subStarted = true;
422
+ }
423
+ },
424
+ isComplete(state) {
425
+ return state.currentIdx >= (state.sequence || []).length;
426
+ },
427
+ cleanup() {},
428
+ };
429
+ break;
430
+
431
+ default:
432
+ console.warn(`[AIController] 알 수 없는 이동 패턴 타입: ${definition.type}`);
433
+ return;
434
+ }
435
+
436
+ PetEngine.registerMovement(name, handler);
437
+ console.log(`[AIController] AI 이동 패턴 등록됨: ${name} (${definition.type})`);
438
+ }
439
+
146
440
  /**
147
441
  * AI 종합 의사결정 실행
148
442
  * OpenClaw이 상황을 분석하고 내린 복합적 결정
@@ -71,6 +71,16 @@
71
71
  // 시간 인식 초기화 (자율 모드에서만 주도적으로 동작)
72
72
  TimeAware.init();
73
73
 
74
+ // 메트릭 수집기 초기화 (선택적 — 없어도 앱 정상 동작)
75
+ if (typeof Metrics !== 'undefined') {
76
+ Metrics.init();
77
+ }
78
+
79
+ // 브라우저 감시 초기화 (참견쟁이 모드)
80
+ if (typeof BrowserWatcher !== 'undefined') {
81
+ BrowserWatcher.init();
82
+ }
83
+
74
84
  // 엔진 시작
75
85
  PetEngine.start();
76
86