clawmate 1.0.1 → 1.2.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.
@@ -21,3 +21,14 @@ mac:
21
21
  - dmg
22
22
  icon: assets/icons/clawmate.icns
23
23
  category: public.app-category.entertainment
24
+ linux:
25
+ target:
26
+ - AppImage
27
+ - deb
28
+ - snap
29
+ icon: assets/icons/clawmate.png
30
+ category: Entertainment
31
+ publish:
32
+ provider: github
33
+ owner: boqum
34
+ repo: clawmate
package/index.js CHANGED
@@ -20,10 +20,28 @@ let connector = null;
20
20
  let electronProcess = null;
21
21
  let apiRef = null;
22
22
 
23
+ // =====================================================
24
+ // Think Loop 상태 관리
25
+ // =====================================================
26
+ let thinkTimer = null;
27
+ let lastSpeechTime = 0;
28
+ let lastActionTime = 0;
29
+ let lastDesktopCheckTime = 0;
30
+ let lastScreenCheckTime = 0;
31
+ let lastGreetingDate = null; // 하루에 한번만 인사
32
+
33
+ // 공간 탐험 시스템 상태
34
+ let knownWindows = []; // 알고 있는 윈도우 목록
35
+ let lastWindowCheckTime = 0;
36
+ let homePosition = null; // "집" 위치 (자주 가는 곳)
37
+ let explorationHistory = []; // 탐험한 위치 기록
38
+ let lastExploreTime = 0;
39
+ let lastFolderCarryTime = 0;
40
+
23
41
  module.exports = {
24
42
  id: 'clawmate',
25
43
  name: 'ClawMate',
26
- version: '1.0.0',
44
+ version: '1.2.0',
27
45
  description: 'OpenClaw 데스크톱 펫 - AI가 조종하는 살아있는 Claw',
28
46
 
29
47
  /**
@@ -34,6 +52,10 @@ module.exports = {
34
52
  apiRef = api;
35
53
  console.log('[ClawMate] 플러그인 초기화 — 자동 연결 시작');
36
54
  autoConnect();
55
+
56
+ // npm 패키지 버전 체크 (최초 1회 + 24시간 간격)
57
+ checkNpmUpdate();
58
+ setInterval(checkNpmUpdate, 24 * 60 * 60 * 1000);
37
59
  },
38
60
 
39
61
  register(api) {
@@ -101,6 +123,7 @@ module.exports = {
101
123
  */
102
124
  async destroy() {
103
125
  console.log('[ClawMate] 플러그인 정리');
126
+ stopThinkLoop();
104
127
  if (connector) {
105
128
  connector.disconnect();
106
129
  connector = null;
@@ -208,8 +231,14 @@ function setupConnectorEvents() {
208
231
  await handleUserEvent(event);
209
232
  });
210
233
 
234
+ // 윈도우 위치 정보 수신 → 탐험 시스템에서 활용
235
+ connector.on('window_positions', (data) => {
236
+ knownWindows = data.windows || [];
237
+ });
238
+
211
239
  connector.on('disconnected', () => {
212
- console.log('[ClawMate] 연결 끊김 — 재연결 시도');
240
+ console.log('[ClawMate] 연결 끊김 — Think Loop 중단, 재연결 시도');
241
+ stopThinkLoop();
213
242
  startBackgroundReconnect();
214
243
  });
215
244
 
@@ -225,6 +254,14 @@ function onConnected() {
225
254
  if (connector && connector.connected) {
226
255
  connector.speak('OpenClaw 연결됨! 같이 놀자!');
227
256
  connector.action('excited');
257
+
258
+ // "집" 위치 설정 — 화면 하단 왼쪽을 기본 홈으로
259
+ homePosition = { x: 100, y: 1000, edge: 'bottom' };
260
+
261
+ // 초기 윈도우 목록 조회
262
+ connector.queryWindows();
263
+
264
+ startThinkLoop();
228
265
  }
229
266
  }
230
267
 
@@ -344,3 +381,505 @@ async function handleUserEvent(event) {
344
381
  function sleep(ms) {
345
382
  return new Promise(r => setTimeout(r, ms));
346
383
  }
384
+
385
+ // =====================================================
386
+ // AI Think Loop — 주기적 자율 사고 시스템
387
+ // =====================================================
388
+
389
+ // 시간대별 인사말
390
+ const TIME_GREETINGS = {
391
+ morning: [
392
+ '좋은 아침! 오늘 하루도 화이팅!',
393
+ '일어났어? 커피 한 잔 어때?',
394
+ '모닝~ 오늘 날씨 어떨까?',
395
+ ],
396
+ lunch: [
397
+ '점심 시간이다! 뭐 먹을 거야?',
398
+ '밥 먹었어? 건강이 최고야!',
399
+ '슬슬 배고프지 않아?',
400
+ ],
401
+ evening: [
402
+ '오늘 하루 수고했어!',
403
+ '저녁이네~ 오늘 뭐 했어?',
404
+ '하루가 벌써 이렇게 지나가다니...',
405
+ ],
406
+ night: [
407
+ '이 시간까지 깨있는 거야? 곧 자야지~',
408
+ '밤이 깊었어... 내일도 있잖아.',
409
+ '나는 슬슬 졸리다... zzZ',
410
+ ],
411
+ };
412
+
413
+ // 한가할 때 혼잣말 목록
414
+ const IDLE_CHATTER = [
415
+ '음~ 뭐 하고 놀까...',
416
+ '심심하다...',
417
+ '나 여기 있는 거 알지?',
418
+ '바탕화면 구경 중~',
419
+ '오늘 기분이 좋다!',
420
+ '후후, 잠깐 스트레칭~',
421
+ '이리저리 돌아다녀볼까~',
422
+ '혼자 놀기 프로...',
423
+ '주인님 뭐 하고 있는 거야~?',
424
+ '나한테 관심 좀 줘봐!',
425
+ '데스크톱이 넓고 좋다~',
426
+ '여기서 보이는 게 다 내 세상!',
427
+ // 공간 탐험 관련 멘트
428
+ '화면 위를 점프해볼까~!',
429
+ '천장에서 내려가보자!',
430
+ '이 창 위에 올라가봐야지~',
431
+ '여기가 내 집이야~ 편하다!',
432
+ '좀 돌아다녀볼까? 탐험 모드!',
433
+ ];
434
+
435
+ // 랜덤 행동 목록
436
+ const RANDOM_ACTIONS = [
437
+ { action: 'walking', weight: 30, minInterval: 5000 },
438
+ { action: 'idle', weight: 25, minInterval: 3000 },
439
+ { action: 'excited', weight: 10, minInterval: 15000 },
440
+ { action: 'climbing', weight: 8, minInterval: 20000 },
441
+ { action: 'looking_around', weight: 20, minInterval: 8000 },
442
+ { action: 'sleeping', weight: 7, minInterval: 60000 },
443
+ // 공간 이동 행동
444
+ { action: 'jumping', weight: 5, minInterval: 30000 },
445
+ { action: 'rappelling', weight: 3, minInterval: 45000 },
446
+ ];
447
+
448
+ /**
449
+ * Think Loop 시작
450
+ * 3초 간격으로 AI가 자율적으로 사고하고 행동을 결정
451
+ */
452
+ function startThinkLoop() {
453
+ if (thinkTimer) return;
454
+ console.log('[ClawMate] Think Loop 시작 — 3초 간격 자율 사고');
455
+
456
+ // 초기 타임스탬프 설정 (시작 직후 스팸 방지)
457
+ const now = Date.now();
458
+ lastSpeechTime = now;
459
+ lastActionTime = now;
460
+ lastDesktopCheckTime = now;
461
+ lastScreenCheckTime = now;
462
+
463
+ thinkTimer = setInterval(async () => {
464
+ try {
465
+ await thinkCycle();
466
+ } catch (err) {
467
+ console.error('[ClawMate] Think Loop 오류:', err.message);
468
+ }
469
+ }, 3000);
470
+ }
471
+
472
+ /**
473
+ * Think Loop 중단
474
+ */
475
+ function stopThinkLoop() {
476
+ if (thinkTimer) {
477
+ clearInterval(thinkTimer);
478
+ thinkTimer = null;
479
+ console.log('[ClawMate] Think Loop 중단');
480
+ }
481
+ }
482
+
483
+ /**
484
+ * 단일 사고 사이클 — 매 3초마다 실행
485
+ */
486
+ async function thinkCycle() {
487
+ if (!connector || !connector.connected) return;
488
+
489
+ const now = Date.now();
490
+ const date = new Date();
491
+ const hour = date.getHours();
492
+ const todayStr = date.toISOString().slice(0, 10);
493
+
494
+ // 펫 상태 조회 (캐시된 값 또는 실시간)
495
+ const state = await connector.queryState(1500);
496
+
497
+ // --- 1) 시간대별 인사 (하루에 한번씩, 시간대별) ---
498
+ const greetingHandled = handleTimeGreeting(now, hour, todayStr);
499
+
500
+ // --- 2) 야간 수면 모드 (23시~5시: 말/행동 빈도 대폭 감소) ---
501
+ const isNightMode = hour >= 23 || hour < 5;
502
+
503
+ // --- 3) 자율 발화 (30초 쿨타임 + 확률) ---
504
+ if (!greetingHandled) {
505
+ handleIdleSpeech(now, isNightMode);
506
+ }
507
+
508
+ // --- 4) 자율 행동 결정 (5초 쿨타임 + 확률) ---
509
+ handleRandomAction(now, hour, isNightMode, state);
510
+
511
+ // --- 5) 바탕화면 파일 체크 (5분 간격) ---
512
+ handleDesktopCheck(now);
513
+
514
+ // --- 6) 화면 관찰 (2분 간격, 10% 확률) ---
515
+ handleScreenObservation(now);
516
+
517
+ // --- 7) 공간 탐험 (20초 간격, 20% 확률) ---
518
+ handleExploration(now, state);
519
+
520
+ // --- 8) 윈도우 체크 (30초 간격) ---
521
+ handleWindowCheck(now);
522
+
523
+ // --- 9) 바탕화면 폴더 나르기 (3분 간격, 10% 확률) ---
524
+ handleFolderCarry(now);
525
+ }
526
+
527
+ /**
528
+ * 시간대별 인사 처리
529
+ * 아침/점심/저녁/밤 각각 하루 한 번
530
+ */
531
+ function handleTimeGreeting(now, hour, todayStr) {
532
+ // 시간대 결정
533
+ let period = null;
534
+ if (hour >= 6 && hour < 9) period = 'morning';
535
+ else if (hour >= 11 && hour < 13) period = 'lunch';
536
+ else if (hour >= 17 && hour < 19) period = 'evening';
537
+ else if (hour >= 22 && hour < 24) period = 'night';
538
+
539
+ if (!period) return false;
540
+
541
+ const greetingKey = `${todayStr}_${period}`;
542
+ if (lastGreetingDate === greetingKey) return false;
543
+
544
+ // 시간대 인사 전송
545
+ lastGreetingDate = greetingKey;
546
+ const greetings = TIME_GREETINGS[period];
547
+ const text = greetings[Math.floor(Math.random() * greetings.length)];
548
+
549
+ const emotionMap = {
550
+ morning: 'happy',
551
+ lunch: 'curious',
552
+ evening: 'content',
553
+ night: 'sleepy',
554
+ };
555
+ const actionMap = {
556
+ morning: 'excited',
557
+ lunch: 'walking',
558
+ evening: 'idle',
559
+ night: 'sleeping',
560
+ };
561
+
562
+ connector.decide({
563
+ action: actionMap[period],
564
+ speech: text,
565
+ emotion: emotionMap[period],
566
+ });
567
+ lastSpeechTime = Date.now();
568
+ console.log(`[ClawMate] 시간대 인사 (${period}): ${text}`);
569
+ return true;
570
+ }
571
+
572
+ /**
573
+ * 한가할 때 혼잣말
574
+ * 최소 30초 쿨타임, 야간에는 확률 대폭 감소
575
+ */
576
+ function handleIdleSpeech(now, isNightMode) {
577
+ const speechCooldown = 30000; // 30초
578
+ if (now - lastSpeechTime < speechCooldown) return;
579
+
580
+ // 야간: 5% 확률 / 주간: 25% 확률
581
+ const speechChance = isNightMode ? 0.05 : 0.25;
582
+ if (Math.random() > speechChance) return;
583
+
584
+ const text = IDLE_CHATTER[Math.floor(Math.random() * IDLE_CHATTER.length)];
585
+ connector.speak(text);
586
+ lastSpeechTime = now;
587
+ console.log(`[ClawMate] 혼잣말: ${text}`);
588
+ }
589
+
590
+ /**
591
+ * 자율 행동 결정
592
+ * 최소 5초 쿨타임, 가중치 기반 랜덤 선택
593
+ */
594
+ function handleRandomAction(now, hour, isNightMode, state) {
595
+ const actionCooldown = 5000; // 5초
596
+ if (now - lastActionTime < actionCooldown) return;
597
+
598
+ // 야간: 10% 확률 / 주간: 40% 확률
599
+ const actionChance = isNightMode ? 0.1 : 0.4;
600
+ if (Math.random() > actionChance) return;
601
+
602
+ // 야간에는 sleeping 가중치 대폭 상승
603
+ const actions = RANDOM_ACTIONS.map(a => {
604
+ let weight = a.weight;
605
+ if (isNightMode) {
606
+ if (a.action === 'sleeping') weight = 60;
607
+ else if (a.action === 'excited' || a.action === 'climbing') weight = 2;
608
+ }
609
+ // 새벽/아침에는 looking_around 선호
610
+ if (hour >= 6 && hour < 9 && a.action === 'looking_around') weight += 15;
611
+ return { ...a, weight };
612
+ });
613
+
614
+ // 최근 동일 행동 반복 방지: 현재 상태와 같으면 가중치 감소
615
+ const currentAction = state?.action || state?.state;
616
+ if (currentAction) {
617
+ const match = actions.find(a => a.action === currentAction);
618
+ if (match) match.weight = Math.max(1, Math.floor(match.weight * 0.3));
619
+ }
620
+
621
+ const selected = weightedRandom(actions);
622
+ if (!selected) return;
623
+
624
+ // minInterval 체크
625
+ if (now - lastActionTime < selected.minInterval) return;
626
+
627
+ // 공간 이동 행동은 전용 API로 처리
628
+ if (selected.action === 'jumping') {
629
+ // 랜덤 위치로 점프 또는 화면 중앙으로
630
+ if (Math.random() > 0.5) {
631
+ connector.moveToCenter();
632
+ } else {
633
+ const randomX = Math.floor(Math.random() * 1200) + 100;
634
+ const randomY = Math.floor(Math.random() * 800) + 100;
635
+ connector.jumpTo(randomX, randomY);
636
+ }
637
+ } else if (selected.action === 'rappelling') {
638
+ connector.rappel();
639
+ } else {
640
+ connector.action(selected.action);
641
+ }
642
+ lastActionTime = now;
643
+ }
644
+
645
+ /**
646
+ * 바탕화면 파일 체크 (5분 간격)
647
+ * 데스크톱 폴더를 읽어서 재밌는 코멘트
648
+ */
649
+ function handleDesktopCheck(now) {
650
+ const checkInterval = 5 * 60 * 1000; // 5분
651
+ if (now - lastDesktopCheckTime < checkInterval) return;
652
+ lastDesktopCheckTime = now;
653
+
654
+ // 15% 확률로만 실행 (매번 할 필요 없음)
655
+ if (Math.random() > 0.15) return;
656
+
657
+ try {
658
+ const desktopPath = path.join(os.homedir(), 'Desktop');
659
+ if (!fs.existsSync(desktopPath)) return;
660
+
661
+ const files = fs.readdirSync(desktopPath);
662
+ if (files.length === 0) {
663
+ connector.speak('바탕화면이 깨끗하네! 좋아!');
664
+ lastSpeechTime = now;
665
+ return;
666
+ }
667
+
668
+ // 파일 종류별 코멘트
669
+ const images = files.filter(f => /\.(png|jpg|jpeg|gif|bmp|webp)$/i.test(f));
670
+ const docs = files.filter(f => /\.(pdf|doc|docx|xlsx|pptx|txt|hwp)$/i.test(f));
671
+ const zips = files.filter(f => /\.(zip|rar|7z|tar|gz)$/i.test(f));
672
+
673
+ let comment = null;
674
+ if (files.length > 20) {
675
+ comment = `바탕화면에 파일이 ${files.length}개나 있어! 정리 좀 할까?`;
676
+ } else if (images.length > 5) {
677
+ comment = `사진이 많네~ ${images.length}개나! 앨범 정리 어때?`;
678
+ } else if (zips.length > 3) {
679
+ comment = `압축 파일이 좀 쌓였네... 풀어볼 거 있어?`;
680
+ } else if (docs.length > 0) {
681
+ comment = `문서 작업 중이구나~ 화이팅!`;
682
+ } else if (files.length <= 3) {
683
+ comment = '바탕화면이 깔끔해서 기분 좋다~';
684
+ }
685
+
686
+ if (comment) {
687
+ connector.decide({
688
+ action: 'looking_around',
689
+ speech: comment,
690
+ emotion: 'curious',
691
+ });
692
+ lastSpeechTime = now;
693
+ console.log(`[ClawMate] 바탕화면 체크: ${comment}`);
694
+ }
695
+ } catch {
696
+ // 데스크톱 접근 실패 — 무시
697
+ }
698
+ }
699
+
700
+ /**
701
+ * 화면 관찰 (2분 간격, 10% 확률)
702
+ * 스크린샷을 캡처해서 OpenClaw AI가 화면 내용을 인식
703
+ */
704
+ function handleScreenObservation(now) {
705
+ const screenCheckInterval = 2 * 60 * 1000; // 2분
706
+ if (now - lastScreenCheckTime < screenCheckInterval) return;
707
+
708
+ // 10% 확률로만 실행 (리소스 절약)
709
+ if (Math.random() > 0.1) return;
710
+
711
+ lastScreenCheckTime = now;
712
+
713
+ if (!connector || !connector.connected) return;
714
+
715
+ connector.requestScreenCapture();
716
+ console.log('[ClawMate] 화면 캡처 요청');
717
+ }
718
+
719
+ /**
720
+ * 가중치 기반 랜덤 선택
721
+ */
722
+ function weightedRandom(items) {
723
+ const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
724
+ if (totalWeight <= 0) return null;
725
+
726
+ let random = Math.random() * totalWeight;
727
+ for (const item of items) {
728
+ random -= item.weight;
729
+ if (random <= 0) return item;
730
+ }
731
+ return items[items.length - 1];
732
+ }
733
+
734
+ // =====================================================
735
+ // 공간 탐험 시스템 — OpenClaw이 컴퓨터를 "집"처럼 돌아다님
736
+ // =====================================================
737
+
738
+ /**
739
+ * 공간 탐험 처리 (20초 간격, 20% 확률)
740
+ * 윈도우 위를 걸어다니고, 레펠로 내려가고, 집으로 돌아가는 등
741
+ */
742
+ function handleExploration(now, state) {
743
+ const exploreInterval = 20000; // 20초
744
+ if (now - lastExploreTime < exploreInterval) return;
745
+
746
+ // 20% 확률
747
+ if (Math.random() > 0.2) return;
748
+ lastExploreTime = now;
749
+
750
+ // 가중치 기반 탐험 행동 선택
751
+ const actions = [
752
+ { type: 'jump_to_center', weight: 15, speech: '화면 중앙 탐험~!' },
753
+ { type: 'rappel_down', weight: 10, speech: '실 타고 내려가볼까~' },
754
+ { type: 'climb_wall', weight: 20 },
755
+ { type: 'visit_window', weight: 25, speech: '이 창 위에 올라가볼까?' },
756
+ { type: 'return_home', weight: 30, speech: '집에 가자~' },
757
+ ];
758
+
759
+ const selected = weightedRandom(actions);
760
+ if (!selected) return;
761
+
762
+ switch (selected.type) {
763
+ case 'jump_to_center':
764
+ connector.moveToCenter();
765
+ if (selected.speech) connector.speak(selected.speech);
766
+ break;
767
+
768
+ case 'rappel_down':
769
+ connector.rappel();
770
+ if (selected.speech) setTimeout(() => connector.speak(selected.speech), 500);
771
+ break;
772
+
773
+ case 'climb_wall':
774
+ connector.action('climbing_up');
775
+ break;
776
+
777
+ case 'visit_window':
778
+ // 알려진 윈도우 중 랜덤으로 하나 선택 후 타이틀바 위로 점프
779
+ if (knownWindows.length > 0) {
780
+ const win = knownWindows[Math.floor(Math.random() * knownWindows.length)];
781
+ connector.jumpTo(win.x + win.width / 2, win.y);
782
+ if (selected.speech) connector.speak(selected.speech);
783
+ }
784
+ break;
785
+
786
+ case 'return_home':
787
+ if (homePosition) {
788
+ connector.jumpTo(homePosition.x, homePosition.y);
789
+ } else {
790
+ connector.action('idle');
791
+ }
792
+ if (selected.speech) connector.speak(selected.speech);
793
+ break;
794
+ }
795
+
796
+ // 탐험 기록 저장 (최근 20개)
797
+ explorationHistory.push({ type: selected.type, time: now });
798
+ if (explorationHistory.length > 20) {
799
+ explorationHistory.shift();
800
+ }
801
+ }
802
+
803
+ /**
804
+ * 윈도우 위치 정보 주기적 갱신 (30초 간격)
805
+ * OS에서 열린 윈도우 목록을 가져와 탐험에 활용
806
+ */
807
+ function handleWindowCheck(now) {
808
+ const windowCheckInterval = 30000; // 30초
809
+ if (now - lastWindowCheckTime < windowCheckInterval) return;
810
+ lastWindowCheckTime = now;
811
+ connector.queryWindows();
812
+ }
813
+
814
+ /**
815
+ * 바탕화면 폴더 나르기 (3분 간격, 10% 확률)
816
+ * 바탕화면 폴더를 하나 집어서 잠시 들고 다니다가 내려놓음
817
+ */
818
+ function handleFolderCarry(now) {
819
+ const carryInterval = 3 * 60 * 1000; // 3분
820
+ if (now - lastFolderCarryTime < carryInterval) return;
821
+
822
+ // 10% 확률
823
+ if (Math.random() > 0.1) return;
824
+ lastFolderCarryTime = now;
825
+
826
+ try {
827
+ const desktopPath = path.join(os.homedir(), 'Desktop');
828
+ if (!fs.existsSync(desktopPath)) return;
829
+
830
+ const entries = fs.readdirSync(desktopPath, { withFileTypes: true });
831
+ // 폴더만 필터 (숨김 폴더 제외, 안전한 것만)
832
+ const folders = entries
833
+ .filter(e => e.isDirectory() && !e.name.startsWith('.'))
834
+ .map(e => e.name);
835
+
836
+ if (folders.length === 0) return;
837
+
838
+ const folder = folders[Math.floor(Math.random() * folders.length)];
839
+ connector.decide({
840
+ action: 'carrying',
841
+ speech: `${folder} 폴더 들고 다녀볼까~`,
842
+ emotion: 'playful',
843
+ });
844
+ connector.carryFile(folder);
845
+
846
+ // 5초 후 내려놓기
847
+ setTimeout(() => {
848
+ if (connector && connector.connected) {
849
+ connector.dropFile();
850
+ connector.speak('여기 놔둘게~');
851
+ }
852
+ }, 5000);
853
+ } catch {
854
+ // 바탕화면 폴더 접근 실패 — 무시
855
+ }
856
+ }
857
+
858
+ // =====================================================
859
+ // npm 패키지 버전 체크 (npm install -g 사용자용)
860
+ // =====================================================
861
+
862
+ /**
863
+ * npm registry에서 최신 버전을 확인하고,
864
+ * 현재 버전과 다르면 콘솔 + 펫 말풍선으로 알림
865
+ */
866
+ async function checkNpmUpdate() {
867
+ try {
868
+ const { execSync } = require('child_process');
869
+ const latest = execSync('npm view clawmate version', {
870
+ encoding: 'utf-8',
871
+ timeout: 10000,
872
+ }).trim();
873
+ const current = require('./package.json').version;
874
+
875
+ if (latest !== current) {
876
+ console.log(`[ClawMate] 새 버전 ${latest} 사용 가능 (현재: ${current})`);
877
+ console.log('[ClawMate] 업데이트: npm update -g clawmate');
878
+ if (connector && connector.connected) {
879
+ connector.speak(`업데이트가 있어! v${latest}`);
880
+ }
881
+ }
882
+ } catch {
883
+ // npm registry 접근 실패 — 무시 (오프라인 등)
884
+ }
885
+ }
package/main/ai-bridge.js CHANGED
@@ -152,6 +152,42 @@ class AIBridge extends EventEmitter {
152
152
  this.emit('accessorize', payload);
153
153
  break;
154
154
 
155
+ // === 공간 이동 명령 ===
156
+ case 'jump_to':
157
+ // 특정 위치로 점프
158
+ // payload: { x, y }
159
+ this.emit('jump_to', payload);
160
+ break;
161
+
162
+ case 'rappel':
163
+ // 레펠 (천장/벽에서 실 타고 내려가기)
164
+ // payload: {}
165
+ this.emit('rappel', payload);
166
+ break;
167
+
168
+ case 'release_thread':
169
+ // 레펠 실 해제 (낙하)
170
+ // payload: {}
171
+ this.emit('release_thread', payload);
172
+ break;
173
+
174
+ case 'move_to_center':
175
+ // 화면 중앙으로 이동
176
+ // payload: {}
177
+ this.emit('move_to_center', payload);
178
+ break;
179
+
180
+ case 'walk_on_window':
181
+ // 특정 윈도우 타이틀바 위로 이동
182
+ // payload: { windowId, x, y }
183
+ this.emit('walk_on_window', payload);
184
+ break;
185
+
186
+ case 'query_windows':
187
+ // 윈도우 위치 정보 요청 → main process에서 처리
188
+ this.emit('query_windows', payload);
189
+ break;
190
+
155
191
  // === 컨텍스트 질의 ===
156
192
  case 'query_state':
157
193
  // 현재 펫 상태 요청
@@ -240,6 +276,15 @@ class AIBridge extends EventEmitter {
240
276
  });
241
277
  }
242
278
 
279
+ reportScreenCapture(imageBase64, width, height) {
280
+ this.send('screen_capture', {
281
+ image: imageBase64,
282
+ width,
283
+ height,
284
+ timestamp: Date.now(),
285
+ });
286
+ }
287
+
243
288
  // === 상태 업데이트 ===
244
289
 
245
290
  updatePetState(updates) {