clawmate 1.3.0 → 1.4.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.
@@ -1,4 +1,4 @@
1
- appId: dev.openclaw.clawmate
1
+ appId: dev.clawmate.app
2
2
  productName: ClawMate
3
3
  directories:
4
4
  output: dist
package/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
- * ClawMate — OpenClaw 플러그인 진입점
2
+ * ClawMate 플러그인 진입점
3
3
  *
4
- * 핵심 원칙: OpenClaw이 켜지면 자동으로 ClawMate를 찾아서 연결.
4
+ * 핵심 원칙: AI가 연결되면 자동으로 ClawMate를 찾아서 연결.
5
5
  *
6
6
  * 흐름:
7
- * OpenClaw 시작 → 플러그인 로드 → init() 자동 호출
7
+ * 플러그인 로드 → init() 자동 호출
8
8
  * → ClawMate 실행 중인지 확인 (ws://127.0.0.1:9320 연결 시도)
9
9
  * → 실행 중이면: 바로 연결, AI 뇌 역할 시작
10
10
  * → 안 돌고 있으면: Electron 앱 자동 실행 → 연결
@@ -14,7 +14,7 @@ const { spawn } = require('child_process');
14
14
  const path = require('path');
15
15
  const os = require('os');
16
16
  const fs = require('fs');
17
- const { OpenClawConnector } = require('./main/ai-connector');
17
+ const { ClawMateConnector } = require('./main/ai-connector');
18
18
 
19
19
  let connector = null;
20
20
  let electronProcess = null;
@@ -61,14 +61,18 @@ let behaviorAdjustments = { // 현재 적용 중인 행동 조정값
61
61
  };
62
62
  let lastMetricsLogTime = 0; // 마지막 품질 보고서 로그 시각
63
63
 
64
+ // AI 모션 생성 시스템 상태
65
+ let lastMotionGenTime = 0; // 마지막 모션 생성 시각
66
+ let generatedMotionCount = 0; // 생성된 모션 수
67
+
64
68
  module.exports = {
65
69
  id: 'clawmate',
66
70
  name: 'ClawMate',
67
- version: '1.3.0',
68
- description: 'OpenClaw 데스크톱 펫 - AI가 조종하는 살아있는 Claw',
71
+ version: '1.4.0',
72
+ description: 'ClawMate 데스크톱 펫 - AI가 조종하는 살아있는 ',
69
73
 
70
74
  /**
71
- * OpenClaw이 플러그인을 로드할 자동 호출
75
+ * 플러그인 로드 자동 호출
72
76
  * → ClawMate 자동 실행 + 자동 연결
73
77
  */
74
78
  async init(api) {
@@ -170,7 +174,7 @@ module.exports = {
170
174
  },
171
175
 
172
176
  /**
173
- * OpenClaw 종료 시 정리
177
+ * 플러그인 종료 시 정리
174
178
  */
175
179
  async destroy() {
176
180
  console.log('[ClawMate] 플러그인 정리');
@@ -188,7 +192,7 @@ module.exports = {
188
192
  // =====================================================
189
193
 
190
194
  /**
191
- * OpenClaw 시작 시 자동으로 ClawMate 찾기/실행/연결
195
+ * 플러그인 시작 시 자동으로 ClawMate 찾기/실행/연결
192
196
  * 무한 재시도 — ClawMate가 살아있는 한 항상 연결 유지
193
197
  */
194
198
  async function autoConnect() {
@@ -214,7 +218,7 @@ async function autoConnect() {
214
218
  function tryConnect() {
215
219
  return new Promise((resolve) => {
216
220
  if (!connector) {
217
- connector = new OpenClawConnector(9320);
221
+ connector = new ClawMateConnector(9320);
218
222
  setupConnectorEvents();
219
223
  }
220
224
 
@@ -308,7 +312,7 @@ function setupConnectorEvents() {
308
312
  */
309
313
  function onConnected() {
310
314
  if (connector && connector.connected) {
311
- connector.speak('OpenClaw 연결됨! 같이 놀자!');
315
+ connector.speak('AI 연결됨! 같이 놀자!');
312
316
  connector.action('excited');
313
317
 
314
318
  // "집" 위치 설정 — 화면 하단 왼쪽을 기본 홈으로
@@ -387,6 +391,14 @@ async function handleUserEvent(event) {
387
391
  }
388
392
  break;
389
393
 
394
+ case 'double_click':
395
+ connector.decide({
396
+ action: 'excited',
397
+ speech: '우와! 더블클릭이다! 기분 좋아~',
398
+ emotion: 'happy',
399
+ });
400
+ break;
401
+
390
402
  case 'drag':
391
403
  connector.speak('으앗, 나를 옮기다니!');
392
404
  break;
@@ -1076,6 +1088,9 @@ async function thinkCycle() {
1076
1088
 
1077
1089
  // --- 9) 바탕화면 폴더 나르기 (3분 간격, 10% 확률) ---
1078
1090
  handleFolderCarry(now);
1091
+
1092
+ // --- 10) AI 모션 생성 (2분 간격, 15% 확률) ---
1093
+ handleMotionGeneration(now, state);
1079
1094
  }
1080
1095
 
1081
1096
  /**
@@ -1253,7 +1268,7 @@ function handleDesktopCheck(now) {
1253
1268
 
1254
1269
  /**
1255
1270
  * 화면 관찰 (2분 간격, 10% 확률)
1256
- * 스크린샷을 캡처해서 OpenClaw AI가 화면 내용을 인식
1271
+ * 스크린샷을 캡처해서 AI가 화면 내용을 인식
1257
1272
  */
1258
1273
  function handleScreenObservation(now) {
1259
1274
  const screenCheckInterval = 2 * 60 * 1000; // 2분
@@ -1286,7 +1301,7 @@ function weightedRandom(items) {
1286
1301
  }
1287
1302
 
1288
1303
  // =====================================================
1289
- // 공간 탐험 시스템 — OpenClaw이 컴퓨터를 "집"처럼 돌아다님
1304
+ // 공간 탐험 시스템 — 펫이 컴퓨터를 "집"처럼 돌아다님
1290
1305
  // =====================================================
1291
1306
 
1292
1307
  /**
@@ -1410,6 +1425,175 @@ function handleFolderCarry(now) {
1410
1425
  }
1411
1426
  }
1412
1427
 
1428
+ // =====================================================
1429
+ // AI 모션 생성 시스템 — 키프레임 기반 움직임을 동적 생성
1430
+ // =====================================================
1431
+
1432
+ /**
1433
+ * AI 모션 생성 처리 (2분 간격, 15% 확률)
1434
+ * 상황에 맞는 커스텀 이동 패턴을 AI가 직접 생성하여 등록+실행
1435
+ *
1436
+ * 생성 전략:
1437
+ * 1차: apiRef.generate()로 완전한 키프레임 데이터 생성
1438
+ * 2차: 상태 기반 프로시저럴 모션 생성 (폴백)
1439
+ */
1440
+ async function handleMotionGeneration(now, state) {
1441
+ const motionGenInterval = 2 * 60 * 1000; // 2분
1442
+ if (now - lastMotionGenTime < motionGenInterval) return;
1443
+ if (Math.random() > 0.15) return; // 15% 확률
1444
+ lastMotionGenTime = now;
1445
+
1446
+ const currentState = state?.action || state?.state || 'idle';
1447
+
1448
+ // AI로 모션 생성 시도
1449
+ let motionDef = null;
1450
+ if (apiRef?.generate) {
1451
+ try {
1452
+ motionDef = await generateMotionWithAI(currentState);
1453
+ } catch {}
1454
+ }
1455
+
1456
+ // 폴백: 프로시저럴 모션 생성
1457
+ if (!motionDef) {
1458
+ motionDef = generateProceduralMotion(currentState, now);
1459
+ }
1460
+
1461
+ if (motionDef && connector?.connected) {
1462
+ const motionName = `ai_motion_${generatedMotionCount++}`;
1463
+ connector.registerMovement(motionName, motionDef);
1464
+
1465
+ // 잠시 후 실행
1466
+ setTimeout(() => {
1467
+ if (connector?.connected) {
1468
+ connector.customMove(motionName, {});
1469
+ console.log(`[ClawMate] AI 모션 생성 실행: ${motionName}`);
1470
+ }
1471
+ }, 500);
1472
+ }
1473
+ }
1474
+
1475
+ /**
1476
+ * AI로 키프레임 모션 생성
1477
+ * 수학 공식(formula) 또는 웨이포인트(waypoints) 방식의 모션 정의를 생성
1478
+ */
1479
+ async function generateMotionWithAI(currentState) {
1480
+ const prompt = `현재 펫 상태: ${currentState}.
1481
+ 이 상황에 어울리는 재미있는 이동 패턴을 JSON으로 만들어줘.
1482
+
1483
+ 두 가지 형식 중 하나를 선택:
1484
+ 1) formula 방식 (수학적 궤도):
1485
+ {"type":"formula","formula":{"xAmp":80,"yAmp":40,"xFreq":1,"yFreq":2,"xPhase":0,"yPhase":0},"duration":3000,"speed":1.5}
1486
+
1487
+ 2) waypoints 방식 (경로점):
1488
+ {"type":"waypoints","waypoints":[{"x":100,"y":200,"pause":300},{"x":300,"y":100},{"x":500,"y":250}],"speed":2}
1489
+
1490
+ 규칙:
1491
+ - xAmp/yAmp: 10~150 사이 (화면 크기 고려)
1492
+ - duration: 2000~6000ms
1493
+ - waypoints: 3~6개
1494
+ - speed: 0.5~3
1495
+ - 펫 성격에 맞게: 장난스럽고 귀여운 움직임
1496
+ JSON만 출력해.`;
1497
+
1498
+ const response = await apiRef.generate(prompt);
1499
+ if (!response || typeof response !== 'string') return null;
1500
+
1501
+ // JSON 파싱
1502
+ let jsonStr = response;
1503
+ const jsonMatch = response.match(/```(?:json)?\s*([\s\S]*?)```/);
1504
+ if (jsonMatch) jsonStr = jsonMatch[1].trim();
1505
+ else {
1506
+ const braceMatch = response.match(/\{[\s\S]*\}/);
1507
+ if (braceMatch) jsonStr = braceMatch[0];
1508
+ }
1509
+
1510
+ try {
1511
+ const def = JSON.parse(jsonStr);
1512
+ // 기본 검증
1513
+ if (def.type === 'formula' && def.formula) {
1514
+ def.duration = Math.min(6000, Math.max(2000, def.duration || 3000));
1515
+ return def;
1516
+ }
1517
+ if (def.type === 'waypoints' && Array.isArray(def.waypoints) && def.waypoints.length >= 2) {
1518
+ return def;
1519
+ }
1520
+ } catch {}
1521
+ return null;
1522
+ }
1523
+
1524
+ /**
1525
+ * 프로시저럴 모션 생성 (AI 없을 때 폴백)
1526
+ * 현재 상태와 시간에 따라 수학적으로 모션 패턴 생성
1527
+ */
1528
+ function generateProceduralMotion(currentState, now) {
1529
+ const hour = new Date(now).getHours();
1530
+ const seed = now % 1000;
1531
+
1532
+ // 상태별 모션 특성
1533
+ const stateMotions = {
1534
+ idle: () => {
1535
+ // 가벼운 좌우 흔들림 또는 작은 원
1536
+ if (seed > 500) {
1537
+ return {
1538
+ type: 'formula',
1539
+ formula: { xAmp: 20 + seed % 30, yAmp: 5 + seed % 10, xFreq: 0.5, yFreq: 1, xPhase: 0, yPhase: Math.PI / 2 },
1540
+ duration: 3000,
1541
+ speed: 0.8,
1542
+ };
1543
+ }
1544
+ return {
1545
+ type: 'formula',
1546
+ formula: { xAmp: 15, yAmp: 15, xFreq: 1, yFreq: 1, xPhase: 0, yPhase: Math.PI / 2 },
1547
+ duration: 2500,
1548
+ speed: 0.6,
1549
+ };
1550
+ },
1551
+ walking: () => {
1552
+ // 지그재그 또는 사인파 이동
1553
+ const amp = 30 + seed % 50;
1554
+ return {
1555
+ type: 'formula',
1556
+ formula: { xAmp: amp, yAmp: amp * 0.3, xFreq: 0.5, yFreq: 2, xPhase: 0, yPhase: 0 },
1557
+ duration: 4000,
1558
+ speed: 1.2,
1559
+ };
1560
+ },
1561
+ excited: () => {
1562
+ // 활발한 8자 궤도
1563
+ return {
1564
+ type: 'formula',
1565
+ formula: { xAmp: 80 + seed % 40, yAmp: 40 + seed % 20, xFreq: 1, yFreq: 2, xPhase: 0, yPhase: 0 },
1566
+ duration: 3000,
1567
+ speed: 2.0,
1568
+ };
1569
+ },
1570
+ playing: () => {
1571
+ // 불규칙한 웨이포인트 (놀기 느낌)
1572
+ const points = [];
1573
+ for (let i = 0; i < 4; i++) {
1574
+ points.push({
1575
+ x: 100 + Math.floor(Math.random() * 800),
1576
+ y: 100 + Math.floor(Math.random() * 400),
1577
+ pause: i === 0 ? 200 : 0,
1578
+ });
1579
+ }
1580
+ return { type: 'waypoints', waypoints: points, speed: 2.5 };
1581
+ },
1582
+ };
1583
+
1584
+ // 야간에는 느린 모션
1585
+ const isNight = hour >= 23 || hour < 6;
1586
+ const generator = stateMotions[currentState] || stateMotions.idle;
1587
+ const motion = generator();
1588
+
1589
+ if (isNight) {
1590
+ motion.speed = Math.min(0.5, (motion.speed || 1) * 0.4);
1591
+ if (motion.duration) motion.duration *= 1.5;
1592
+ }
1593
+
1594
+ return motion;
1595
+ }
1596
+
1413
1597
  // =====================================================
1414
1598
  // 자기 관찰 시스템 (Metrics → 행동 조정)
1415
1599
  // =====================================================
@@ -1586,7 +1770,7 @@ function adjustBehavior(metrics) {
1586
1770
 
1587
1771
  /**
1588
1772
  * 품질 보고서 콘솔 출력 (5분마다)
1589
- * OpenClaw 개발자/운영자가 펫의 동작 품질을 모니터링할 수 있도록 한다.
1773
+ * 개발자/운영자가 펫의 동작 품질을 모니터링할 수 있도록 한다.
1590
1774
  */
1591
1775
  function _logQualityReport(metrics) {
1592
1776
  const adj = behaviorAdjustments;
package/main/ai-bridge.js CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * OpenClaw ↔ ClawMate AI 브릿지
2
+ * AI ↔ ClawMate 브릿지
3
3
  *
4
- * OpenClaw 에이전트가 ClawMate의 뇌 역할을 한다.
5
- * - OpenClaw → ClawMate: 행동 명령, 말풍선, 감정, 이동
6
- * - ClawMate → OpenClaw: 사용자 이벤트 (클릭, 드래그, 커서, 파일 변화)
4
+ * AI 에이전트가 ClawMate의 뇌 역할을 한다.
5
+ * - AI → ClawMate: 행동 명령, 말풍선, 감정, 이동
6
+ * - ClawMate → AI: 사용자 이벤트 (클릭, 드래그, 커서, 파일 변화)
7
7
  *
8
8
  * 통신: WebSocket (로컬 ws://localhost:9320)
9
9
  * 프로토콜: JSON 메시지
10
10
  *
11
- * OpenClaw 연결 안 됐을 때 → 자율 모드 (기존 FSM) 로 폴백
11
+ * AI 연결 안 됐을 때 → 자율 모드 (기존 FSM) 로 폴백
12
12
  */
13
13
  const WebSocket = require('ws');
14
14
  const EventEmitter = require('events');
@@ -17,7 +17,7 @@ class AIBridge extends EventEmitter {
17
17
  constructor() {
18
18
  super();
19
19
  this.wss = null;
20
- this.client = null; // 연결된 OpenClaw 에이전트
20
+ this.client = null; // 연결된 AI 에이전트
21
21
  this.connected = false;
22
22
  this.port = 9320;
23
23
  this.heartbeatInterval = null;
@@ -33,19 +33,19 @@ class AIBridge extends EventEmitter {
33
33
  }
34
34
 
35
35
  /**
36
- * WebSocket 서버 시작 — OpenClaw이 여기에 접속
36
+ * WebSocket 서버 시작 — AI 에이전트가 여기에 접속
37
37
  */
38
38
  start() {
39
39
  this.wss = new WebSocket.Server({ port: this.port, host: '127.0.0.1' });
40
40
 
41
41
  this.wss.on('connection', (ws) => {
42
- console.log('[AI Bridge] OpenClaw 연결됨');
42
+ console.log('[AI Bridge] AI 연결됨');
43
43
  this.client = ws;
44
44
  this.connected = true;
45
45
  this.reconnectAttempts = 0;
46
46
  this.emit('connected');
47
47
 
48
- // OpenClaw에 현재 상태 전송
48
+ // AI에 현재 상태 전송
49
49
  this.send('sync', this.petState);
50
50
 
51
51
  ws.on('message', (data) => {
@@ -58,7 +58,7 @@ class AIBridge extends EventEmitter {
58
58
  });
59
59
 
60
60
  ws.on('close', () => {
61
- console.log('[AI Bridge] OpenClaw 연결 해제');
61
+ console.log('[AI Bridge] AI 연결 해제');
62
62
  this.client = null;
63
63
  this.connected = false;
64
64
  this.emit('disconnected');
@@ -84,7 +84,7 @@ class AIBridge extends EventEmitter {
84
84
  }
85
85
 
86
86
  /**
87
- * OpenClaw에서 온 명령 처리
87
+ * AI에서 온 명령 처리
88
88
  */
89
89
  _handleCommand(msg) {
90
90
  const { type, payload } = msg;
@@ -92,7 +92,7 @@ class AIBridge extends EventEmitter {
92
92
  switch (type) {
93
93
  // === 행동 제어 ===
94
94
  case 'action':
95
- // OpenClaw이 펫의 행동을 직접 지시
95
+ // AI가 펫의 행동을 직접 지시
96
96
  // payload: { state: 'walking'|'excited'|..., duration?: ms }
97
97
  this.emit('action', payload);
98
98
  break;
@@ -111,7 +111,7 @@ class AIBridge extends EventEmitter {
111
111
 
112
112
  // === 말하기 ===
113
113
  case 'speak':
114
- // OpenClaw이 펫을 통해 사용자에게 말함
114
+ // AI가 펫을 통해 사용자에게 말함
115
115
  // payload: { text: string, style?: 'normal'|'thought'|'shout' }
116
116
  this.emit('speak', payload);
117
117
  break;
@@ -196,7 +196,7 @@ class AIBridge extends EventEmitter {
196
196
 
197
197
  // === 커스텀 이동 패턴 ===
198
198
  case 'register_movement':
199
- // OpenClaw이 커스텀 이동 패턴 등록
199
+ // AI가 커스텀 이동 패턴 등록
200
200
  // payload: { name: string, definition: { type: 'waypoints'|'formula'|'sequence', ... } }
201
201
  this.emit('register_movement', payload);
202
202
  break;
@@ -231,6 +231,12 @@ class AIBridge extends EventEmitter {
231
231
  this.emit('reset_character', payload);
232
232
  break;
233
233
 
234
+ case 'set_persona':
235
+ // 봇 인격체 전환 (Incarnation 모드)
236
+ // payload: { name, personality, speakingStyle, color?, ... }
237
+ this.emit('set_persona', payload);
238
+ break;
239
+
234
240
  // === 컨텍스트 질의 ===
235
241
  case 'query_state':
236
242
  // 현재 펫 상태 요청
@@ -244,7 +250,7 @@ class AIBridge extends EventEmitter {
244
250
 
245
251
  // === AI 의사결정 결과 ===
246
252
  case 'ai_decision':
247
- // OpenClaw AI의 종합적 의사결정
253
+ // AI의 종합적 의사결정
248
254
  // payload: { action, speech?, emotion?, reasoning? }
249
255
  this.emit('ai_decision', payload);
250
256
  break;
@@ -255,7 +261,7 @@ class AIBridge extends EventEmitter {
255
261
  }
256
262
 
257
263
  /**
258
- * OpenClaw에 이벤트 전송
264
+ * AI에 이벤트 전송
259
265
  */
260
266
  send(type, payload) {
261
267
  if (!this.client || this.client.readyState !== WebSocket.OPEN) return false;
@@ -267,7 +273,7 @@ class AIBridge extends EventEmitter {
267
273
  }
268
274
  }
269
275
 
270
- // === 사용자 이벤트 리포트 (ClawMate → OpenClaw) ===
276
+ // === 사용자 이벤트 리포트 (ClawMate → AI) ===
271
277
 
272
278
  reportUserClick(position) {
273
279
  this.send('user_event', {
@@ -329,7 +335,7 @@ class AIBridge extends EventEmitter {
329
335
  }
330
336
 
331
337
  /**
332
- * 메트릭 데이터를 OpenClaw에 전송
338
+ * 메트릭 데이터를 AI에 전송
333
339
  * 렌더러에서 수집한 펫 동작 품질 메트릭을 AI에 전달
334
340
  */
335
341
  reportMetrics(summary) {
@@ -1,11 +1,11 @@
1
1
  /**
2
- * OpenClaw 에이전트 측 커넥터
2
+ * AI 에이전트 측 커넥터
3
3
  *
4
- * OpenClaw이 ClawMate에 접속해서 펫을 조종하는 클라이언트.
5
- * OpenClaw 플러그인(index.js)에서 사용됨.
4
+ * AI가 ClawMate에 접속해서 펫을 조종하는 클라이언트.
5
+ * ClawMate 플러그인(index.js)에서 사용됨.
6
6
  *
7
7
  * 사용 예:
8
- * const connector = new OpenClawConnector();
8
+ * const connector = new ClawMateConnector();
9
9
  * await connector.connect();
10
10
  * connector.speak('안녕! 오늘 뭐 할 거야?');
11
11
  * connector.action('excited');
@@ -14,7 +14,7 @@
14
14
  const WebSocket = require('ws');
15
15
  const EventEmitter = require('events');
16
16
 
17
- class OpenClawConnector extends EventEmitter {
17
+ class ClawMateConnector extends EventEmitter {
18
18
  constructor(port = 9320) {
19
19
  super();
20
20
  this.port = port;
@@ -81,12 +81,12 @@ class OpenClawConnector extends EventEmitter {
81
81
  break;
82
82
 
83
83
  case 'user_event':
84
- // 사용자 이벤트 → OpenClaw AI가 반응 결정
84
+ // 사용자 이벤트 → AI가 반응 결정
85
85
  this.emit('user_event', payload);
86
86
  break;
87
87
 
88
88
  case 'screen_capture':
89
- // 화면 캡처 응답 → OpenClaw AI가 분석
89
+ // 화면 캡처 응답 → AI가 분석
90
90
  this.emit('screen_capture', payload);
91
91
  break;
92
92
 
@@ -96,7 +96,7 @@ class OpenClawConnector extends EventEmitter {
96
96
  break;
97
97
 
98
98
  case 'metrics_report':
99
- // 메트릭 데이터 수신 → OpenClaw AI가 분석
99
+ // 메트릭 데이터 수신 → AI가 분석
100
100
  this.emit('metrics_report', payload);
101
101
  break;
102
102
 
@@ -115,7 +115,7 @@ class OpenClawConnector extends EventEmitter {
115
115
  }
116
116
  }
117
117
 
118
- // === OpenClaw → ClawMate 명령 API ===
118
+ // === AI → ClawMate 명령 API ===
119
119
 
120
120
  /** 펫이 말하게 함 */
121
121
  speak(text, style = 'normal') {
@@ -164,13 +164,13 @@ class OpenClawConnector extends EventEmitter {
164
164
 
165
165
  /**
166
166
  * AI 종합 의사결정 전송
167
- * OpenClaw AI가 상황을 분석하고 내린 결정을 한번에 전송
167
+ * AI가 상황을 분석하고 내린 결정을 한번에 전송
168
168
  */
169
169
  decide(decision) {
170
170
  return this._send('ai_decision', decision);
171
171
  }
172
172
 
173
- // === 공간 이동 API (OpenClaw이 컴퓨터를 "집"처럼 돌아다님) ===
173
+ // === 공간 이동 API (펫이 컴퓨터를 "집"처럼 돌아다님) ===
174
174
 
175
175
  /** 특정 위치로 점프 */
176
176
  jumpTo(x, y) {
@@ -270,6 +270,11 @@ class OpenClawConnector extends EventEmitter {
270
270
  return this._send('reset_character', {});
271
271
  }
272
272
 
273
+ /** 인격체 전환 (Incarnation 모드에서 봇 인격 반영) */
274
+ setPersona(data) {
275
+ return this._send('set_persona', data);
276
+ }
277
+
273
278
  /**
274
279
  * 현재 펫 상태 요청 (Promise 반환)
275
280
  * 서버에서 state_response가 오면 resolve, 타임아웃 시 캐시된 상태 반환
@@ -319,4 +324,4 @@ class OpenClawConnector extends EventEmitter {
319
324
  }
320
325
  }
321
326
 
322
- module.exports = { OpenClawConnector };
327
+ module.exports = { ClawMateConnector };
package/main/autostart.js CHANGED
@@ -5,8 +5,8 @@
5
5
  * macOS: LaunchAgent plist
6
6
  * Linux: ~/.config/autostart/ 디렉토리에 .desktop 파일 생성
7
7
  *
8
- * OpenClaw이 나중에 켜져도, ClawMate는 이미 돌고 있어서 바로 연결됨.
9
- * ClawMate가 먼저 혼자 자율 모드로 돌다가 → OpenClaw 연결되면 AI 모드 전환.
8
+ * AI가 나중에 켜져도, ClawMate는 이미 돌고 있어서 바로 연결됨.
9
+ * ClawMate가 먼저 혼자 자율 모드로 돌다가 → AI 연결되면 AI 모드 전환.
10
10
  */
11
11
  const { app } = require('electron');
12
12
  const fs = require('fs');
@@ -22,7 +22,7 @@ function getDesktopFileContent() {
22
22
  '[Desktop Entry]',
23
23
  'Type=Application',
24
24
  'Name=ClawMate',
25
- 'Comment=OpenClaw Desktop Pet',
25
+ 'Comment=ClawMate Desktop Pet',
26
26
  `Exec=${process.execPath} ${path.resolve(__dirname, '..')}`,
27
27
  'X-GNOME-Autostart-enabled=true',
28
28
  'Hidden=false',
@@ -297,20 +297,55 @@ function parseCharacterCommand(text) {
297
297
  * @param {string} text - 사용자 메시지
298
298
  * @returns {{ type: string, ... }}
299
299
  */
300
+ /**
301
+ * 모드/설정 변경 명령 감지
302
+ * @param {string} text - 사용자 메시지
303
+ * @returns {{ type: 'mode_change', mode: string }|{ type: 'setting', key, value }|null}
304
+ */
305
+ function parseSettingCommand(text) {
306
+ if (!text) return null;
307
+ const t = text.trim().toLowerCase();
308
+
309
+ // 모드 변경
310
+ if (/(?:펫|pet)\s*모드/.test(t)) return { type: 'mode_change', mode: 'pet' };
311
+ if (/(?:인카|인격|incarnation|claw)\s*모드/.test(t)) return { type: 'mode_change', mode: 'incarnation' };
312
+ if (/둘\s*다\s*모드|both\s*mode/i.test(t)) return { type: 'mode_change', mode: 'both' };
313
+
314
+ // 캐릭터 프리셋 선택 (트레이 프리셋과 동일)
315
+ const presetMap = {
316
+ '파란|파랑|blue': 'blue', '초록|green': 'green', '보라|purple': 'purple',
317
+ '골드|금색|gold': 'gold', '핑크|pink': 'pink',
318
+ '고양이|cat': 'cat', '로봇|robot': 'robot', '유령|ghost': 'ghost', '드래곤|dragon': 'dragon',
319
+ '기본|default|원래': 'default',
320
+ };
321
+ for (const [pattern, preset] of Object.entries(presetMap)) {
322
+ const regex = new RegExp(`(?:${pattern})\\s*(?:캐릭터|색|색상|으로)?\\s*(?:바꿔|변경|골라|선택|해줘)?`);
323
+ if (regex.test(t)) {
324
+ return { type: 'preset_character', preset };
325
+ }
326
+ }
327
+
328
+ return null;
329
+ }
330
+
300
331
  function parseMessage(text) {
301
- // 0순위: 캐릭터 변경 명령
332
+ // 0순위: 설정/모드 변경 명령
333
+ const settingCmd = parseSettingCommand(text);
334
+ if (settingCmd) return settingCmd;
335
+
336
+ // 1순위: 캐릭터 변경 명령 (AI 생성)
302
337
  const charCmd = parseCharacterCommand(text);
303
338
  if (charCmd) return charCmd;
304
339
 
305
- // 1순위: 파일 조작 명령
340
+ // 2순위: 파일 조작 명령
306
341
  const fileCmd = parseFileCommand(text);
307
342
  if (fileCmd) return fileCmd;
308
343
 
309
- // 2순위: 행동 명령
344
+ // 3순위: 행동 명령
310
345
  const actionCmd = parseActionCommand(text);
311
346
  if (actionCmd) return actionCmd;
312
347
 
313
- // 3순위: 일반 대화 (speak)
348
+ // 4순위: 일반 대화 (speak)
314
349
  return { type: 'speak', text };
315
350
  }
316
351
 
@@ -319,6 +354,7 @@ module.exports = {
319
354
  parseFileCommand,
320
355
  parseActionCommand,
321
356
  parseCharacterCommand,
357
+ parseSettingCommand,
322
358
  resolveSource,
323
359
  AUTO_CATEGORIES,
324
360
  };
package/main/index.js CHANGED
@@ -69,18 +69,18 @@ function createLauncherWindow() {
69
69
  }
70
70
 
71
71
  /**
72
- * AI Bridge 시작 — OpenClaw 에이전트가 접속하면 펫을 조종
72
+ * AI Bridge 시작 — AI 에이전트가 접속하면 펫을 조종
73
73
  */
74
74
  function startAIBridge(win) {
75
75
  aiBridge = new AIBridge();
76
76
  aiBridge.start();
77
77
 
78
- // OpenClaw → ClawMate 명령을 렌더러에 전달
78
+ // AI → ClawMate 명령을 렌더러에 전달
79
79
  const commandTypes = [
80
80
  'action', 'move', 'emote', 'speak', 'think',
81
81
  'carry_file', 'drop_file', 'set_mode', 'evolve',
82
82
  'accessorize', 'ai_decision',
83
- // 공간 이동 명령 (OpenClaw이 집처럼 돌아다니기)
83
+ // 공간 이동 명령 (펫이 집처럼 돌아다니기)
84
84
  'jump_to', 'rappel', 'release_thread', 'move_to_center', 'walk_on_window',
85
85
  // 커스텀 이동 패턴
86
86
  'register_movement', 'custom_move', 'stop_custom_move', 'list_movements',
@@ -88,6 +88,8 @@ function startAIBridge(win) {
88
88
  'smart_file_op',
89
89
  // 캐릭터 커스터마이징 (텔레그램에서 AI 생성)
90
90
  'set_character', 'reset_character',
91
+ // 인격체 전환 (Incarnation 모드)
92
+ 'set_persona',
91
93
  ];
92
94
 
93
95
  commandTypes.forEach((type) => {
@@ -98,7 +100,7 @@ function startAIBridge(win) {
98
100
  });
99
101
  });
100
102
 
101
- // OpenClaw 윈도우 위치 정보 요청 처리
103
+ // AI 윈도우 위치 정보 요청 처리
102
104
  aiBridge.on('query_windows', async () => {
103
105
  try {
104
106
  const { getWindowPositions } = require('./platform');
@@ -110,7 +112,7 @@ function startAIBridge(win) {
110
112
  }
111
113
  });
112
114
 
113
- // OpenClaw 화면 캡처 요청 처리 (main process에서 직접 캡처)
115
+ // AI 화면 캡처 요청 처리 (main process에서 직접 캡처)
114
116
  aiBridge.on('query_screen', async () => {
115
117
  try {
116
118
  const primaryDisplay = screen.getPrimaryDisplay();