clawmate 1.2.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.
package/main/tray.js CHANGED
@@ -1,5 +1,6 @@
1
- const { Tray, Menu, nativeImage, app } = require('electron');
1
+ const { Tray, Menu, nativeImage, app, shell, dialog, clipboard } = 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,29 +8,111 @@ const { isAutoStartEnabled, toggleAutoStart } = require('./autostart');
7
8
  let tray = null;
8
9
  let aiBridge = null;
9
10
 
10
- function setupTray(mainWindow, bridge) {
11
- aiBridge = bridge;
12
- const store = new Store('clawmate-config', { mode: 'pet' });
13
-
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;
11
+ /**
12
+ * 16x16 Claw 픽셀아트 아이콘 생성
13
+ */
14
+ const CLAW_ICON = [
15
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
16
+ [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
17
+ [0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
18
+ [0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
19
+ [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
20
+ [0,0,0,6,1,1,0,0,0,0,1,1,6,0,0,0],
21
+ [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
22
+ [0,0,0,1,1,4,5,1,1,4,5,1,1,0,0,0],
23
+ [0,0,0,1,1,4,5,1,1,4,5,1,1,0,0,0],
24
+ [0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0],
25
+ [0,0,1,2,1,1,1,1,1,1,1,1,2,1,0,0],
26
+ [0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0],
27
+ [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
28
+ [0,0,3,3,3,0,3,3,3,3,0,3,3,3,0,0],
29
+ [0,3,3,3,0,0,0,3,3,0,0,0,3,3,3,0],
30
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
31
+ ];
32
+
33
+ const COLOR_MAP = {
34
+ 0: [0, 0, 0, 0],
35
+ 1: [255, 79, 64, 255],
36
+ 2: [255, 119, 95, 255],
37
+ 3: [58, 10, 13, 255],
38
+ 4: [255, 255, 255, 255],
39
+ 5: [0, 0, 0, 255],
40
+ 6: [255, 79, 64, 255],
41
+ };
42
+
43
+ /**
44
+ * 캐릭터 프리셋 목록
45
+ * 트레이에서 선택하면 set_character 명령으로 렌더러에 전달
46
+ */
47
+ const CHARACTER_PRESETS = {
48
+ default: {
49
+ name: '기본 Claw (빨강)',
50
+ colorMap: { primary: '#ff4f40', secondary: '#ff775f', dark: '#8B4513', eye: '#ffffff', pupil: '#111111', claw: '#ff4f40' },
51
+ },
52
+ blue: {
53
+ name: '파란 Claw',
54
+ colorMap: { primary: '#4488ff', secondary: '#6699ff', dark: '#223388', eye: '#ffffff', pupil: '#111111', claw: '#4488ff' },
55
+ },
56
+ green: {
57
+ name: '초록 Claw',
58
+ colorMap: { primary: '#44cc44', secondary: '#66dd66', dark: '#226622', eye: '#ffffff', pupil: '#111111', claw: '#44cc44' },
59
+ },
60
+ purple: {
61
+ name: '보라 Claw',
62
+ colorMap: { primary: '#8844cc', secondary: '#aa66dd', dark: '#442266', eye: '#ffffff', pupil: '#111111', claw: '#8844cc' },
63
+ },
64
+ gold: {
65
+ name: '골드 Claw',
66
+ colorMap: { primary: '#ffcc00', secondary: '#ffdd44', dark: '#886600', eye: '#ffffff', pupil: '#111111', claw: '#ffcc00' },
67
+ },
68
+ pink: {
69
+ name: '핑크 Claw',
70
+ colorMap: { primary: '#ff69b4', secondary: '#ff8cc4', dark: '#8B3060', eye: '#ffffff', pupil: '#111111', claw: '#ff69b4' },
71
+ },
72
+ cat: {
73
+ name: '고양이',
74
+ colorMap: { primary: '#ff9944', secondary: '#ffbb66', dark: '#663300', eye: '#88ff88', pupil: '#111111', claw: '#ff9944' },
75
+ },
76
+ robot: {
77
+ name: '로봇',
78
+ colorMap: { primary: '#888888', secondary: '#aaaaaa', dark: '#444444', eye: '#66aaff', pupil: '#0044aa', claw: '#66aaff' },
79
+ },
80
+ ghost: {
81
+ name: '유령',
82
+ colorMap: { primary: '#ccccff', secondary: '#eeeeff', dark: '#6666aa', eye: '#ff6666', pupil: '#cc0000', claw: '#ccccff' },
83
+ },
84
+ dragon: {
85
+ name: '드래곤',
86
+ colorMap: { primary: '#cc2222', secondary: '#ff4444', dark: '#661111', eye: '#ffaa00', pupil: '#111111', claw: '#ffaa00' },
87
+ },
88
+ };
89
+
90
+ function createClawIcon() {
91
+ const size = 16;
92
+ const buffer = Buffer.alloc(size * size * 4);
93
+ for (let y = 0; y < size; y++) {
94
+ for (let x = 0; x < size; x++) {
95
+ const code = CLAW_ICON[y][x];
96
+ const color = COLOR_MAP[code] || COLOR_MAP[0];
97
+ const offset = (y * size + x) * 4;
98
+ buffer[offset + 0] = color[0];
99
+ buffer[offset + 1] = color[1];
100
+ buffer[offset + 2] = color[2];
101
+ buffer[offset + 3] = color[3];
29
102
  }
30
- icon = nativeImage.createFromBuffer(buffer, { width: size, height: size });
31
103
  }
104
+ return nativeImage.createFromBuffer(buffer, { width: size, height: size });
105
+ }
32
106
 
107
+ function setupTray(mainWindow, bridge) {
108
+ aiBridge = bridge;
109
+ const store = new Store('clawmate-config', {
110
+ mode: 'pet',
111
+ character: 'default',
112
+ telegramToken: '',
113
+ });
114
+
115
+ const icon = createClawIcon();
33
116
  tray = new Tray(icon);
34
117
  tray.setToolTip('ClawMate - 데스크톱 펫');
35
118
 
@@ -38,38 +121,94 @@ function setupTray(mainWindow, bridge) {
38
121
  const fileInteraction = store.get('fileInteraction') !== false;
39
122
  const aiConnected = aiBridge ? aiBridge.isConnected() : false;
40
123
  const autoStart = isAutoStartEnabled();
124
+ const currentChar = store.get('character') || 'default';
125
+ const hasTelegramToken = !!(store.get('telegramToken'));
126
+
127
+ // 캐릭터 서브메뉴
128
+ const characterSubmenu = Object.entries(CHARACTER_PRESETS).map(([key, preset]) => ({
129
+ label: preset.name,
130
+ type: 'radio',
131
+ checked: currentChar === key,
132
+ click: () => {
133
+ store.set('character', key);
134
+ if (mainWindow && !mainWindow.isDestroyed()) {
135
+ if (key === 'default') {
136
+ mainWindow.webContents.send('ai-command', {
137
+ type: 'reset_character', payload: {},
138
+ });
139
+ } else {
140
+ mainWindow.webContents.send('ai-command', {
141
+ type: 'set_character', payload: {
142
+ colorMap: preset.colorMap,
143
+ speech: `${preset.name}(으)로 변신!`,
144
+ },
145
+ });
146
+ }
147
+ }
148
+ buildAndSet();
149
+ },
150
+ }));
41
151
 
42
152
  return Menu.buildFromTemplate([
43
153
  {
44
- label: `ClawMate (${mode === 'pet' ? 'Clawby' : 'OpenClaw'})`,
154
+ label: `ClawMate (${mode === 'pet' ? 'Clawby' : mode === 'incarnation' ? 'Claw' : 'Clawby + Claw'})`,
45
155
  enabled: false,
46
156
  },
47
157
  {
48
- label: aiConnected ? 'AI: 연결됨' : 'AI: 자율 모드 (대기 중)',
158
+ label: aiConnected ? 'AI: 연결됨' : 'AI: 자율 모드',
49
159
  enabled: false,
50
160
  },
51
161
  { type: 'separator' },
162
+
163
+ // === 모드 선택 ===
52
164
  {
53
- label: 'Pet 모드 (Clawby)',
54
- type: 'radio',
55
- checked: mode === 'pet',
56
- click: () => {
57
- store.set('mode', 'pet');
58
- if (mainWindow) mainWindow.webContents.send('mode-changed', 'pet');
59
- buildAndSet();
60
- },
165
+ label: '모드',
166
+ submenu: [
167
+ {
168
+ label: 'Pet 모드 (Clawby)',
169
+ sublabel: '귀여운 펫을 키우기',
170
+ type: 'radio',
171
+ checked: mode === 'pet',
172
+ click: () => {
173
+ store.set('mode', 'pet');
174
+ if (mainWindow) mainWindow.webContents.send('mode-changed', 'pet');
175
+ buildAndSet();
176
+ },
177
+ },
178
+ {
179
+ label: 'Incarnation 모드 (Claw)',
180
+ sublabel: '봇이 육체를 얻음',
181
+ type: 'radio',
182
+ checked: mode === 'incarnation',
183
+ click: () => {
184
+ store.set('mode', 'incarnation');
185
+ if (mainWindow) mainWindow.webContents.send('mode-changed', 'incarnation');
186
+ buildAndSet();
187
+ },
188
+ },
189
+ {
190
+ label: '둘 다 (Pet + Incarnation)',
191
+ sublabel: '펫도 키우고, 봇 인격도 반영',
192
+ type: 'radio',
193
+ checked: mode === 'both',
194
+ click: () => {
195
+ store.set('mode', 'both');
196
+ if (mainWindow) mainWindow.webContents.send('mode-changed', 'both');
197
+ buildAndSet();
198
+ },
199
+ },
200
+ ],
61
201
  },
202
+
203
+ // === 캐릭터 선택 ===
62
204
  {
63
- label: 'Incarnation 모드 (OpenClaw)',
64
- type: 'radio',
65
- checked: mode === 'incarnation',
66
- click: () => {
67
- store.set('mode', 'incarnation');
68
- if (mainWindow) mainWindow.webContents.send('mode-changed', 'incarnation');
69
- buildAndSet();
70
- },
205
+ label: '캐릭터',
206
+ submenu: characterSubmenu,
71
207
  },
208
+
72
209
  { type: 'separator' },
210
+
211
+ // === 설정 ===
73
212
  {
74
213
  label: '파일 상호작용',
75
214
  type: 'checkbox',
@@ -88,7 +227,89 @@ function setupTray(mainWindow, bridge) {
88
227
  buildAndSet();
89
228
  },
90
229
  },
230
+
231
+ { type: 'separator' },
232
+
233
+ // === 텔레그램 봇 ===
234
+ {
235
+ label: '텔레그램 봇',
236
+ submenu: [
237
+ {
238
+ label: hasTelegramToken ? '봇 토큰: 설정됨' : '봇 토큰: 미설정',
239
+ enabled: false,
240
+ },
241
+ {
242
+ label: '봇 토큰 설정...',
243
+ click: async () => {
244
+ const result = await dialog.showMessageBox({
245
+ type: 'question',
246
+ buttons: ['클립보드에서 붙여넣기', '직접 입력', '취소'],
247
+ title: 'ClawMate 텔레그램 봇',
248
+ message: '텔레그램 봇 토큰을 설정합니다.',
249
+ detail: '@BotFather에서 받은 봇 토큰을 입력하세요.\n현재 클립보드 내용을 붙여넣으려면 "클립보드에서 붙여넣기"를 선택하세요.',
250
+ });
251
+
252
+ let token = null;
253
+ if (result.response === 0) {
254
+ // 클립보드에서 붙여넣기
255
+ token = clipboard.readText().trim();
256
+ } else if (result.response === 1) {
257
+ // prompt가 없으므로 클립보드 안내
258
+ const promptResult = await dialog.showMessageBox({
259
+ type: 'info',
260
+ buttons: ['확인'],
261
+ title: '텔레그램 봇 토큰',
262
+ message: '봇 토큰을 클립보드에 복사한 후 다시 "봇 토큰 설정"을 선택하세요.',
263
+ detail: '텔레그램에서 @BotFather → /newbot → 토큰 복사',
264
+ });
265
+ return;
266
+ } else {
267
+ return;
268
+ }
269
+
270
+ if (token && token.includes(':')) {
271
+ store.set('telegramToken', token);
272
+ process.env.CLAWMATE_TELEGRAM_TOKEN = token;
273
+ buildAndSet();
274
+
275
+ // 펫 알림
276
+ if (mainWindow && !mainWindow.isDestroyed()) {
277
+ mainWindow.webContents.send('ai-command', {
278
+ type: 'speak',
279
+ payload: { text: '텔레그램 봇 토큰 설정 완료!' },
280
+ });
281
+ }
282
+ } else {
283
+ await dialog.showMessageBox({
284
+ type: 'error',
285
+ buttons: ['확인'],
286
+ title: '잘못된 토큰',
287
+ message: '유효한 텔레그램 봇 토큰이 아닙니다.',
288
+ detail: '올바른 형식: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz',
289
+ });
290
+ }
291
+ },
292
+ },
293
+ {
294
+ label: '봇 토큰 제거',
295
+ enabled: hasTelegramToken,
296
+ click: () => {
297
+ store.set('telegramToken', '');
298
+ delete process.env.CLAWMATE_TELEGRAM_TOKEN;
299
+ buildAndSet();
300
+ },
301
+ },
302
+ ],
303
+ },
304
+
91
305
  { type: 'separator' },
306
+
307
+ {
308
+ label: '업데이트 확인',
309
+ click: async () => {
310
+ await checkForUpdateManual(mainWindow);
311
+ },
312
+ },
92
313
  {
93
314
  label: '파일 이동 되돌리기',
94
315
  click: async () => {
@@ -122,4 +343,51 @@ function setupTray(mainWindow, bridge) {
122
343
  return tray;
123
344
  }
124
345
 
346
+ /**
347
+ * 수동 업데이트 확인
348
+ */
349
+ async function checkForUpdateManual(mainWindow) {
350
+ if (app.isPackaged) {
351
+ try {
352
+ const { autoUpdater } = require('electron-updater');
353
+ autoUpdater.checkForUpdatesAndNotify();
354
+ } catch (err) {
355
+ console.error('[업데이트] electron-updater 실패:', err.message);
356
+ }
357
+ } else {
358
+ try {
359
+ const latest = execSync('npm view clawmate version', {
360
+ encoding: 'utf-8',
361
+ timeout: 10000,
362
+ }).trim();
363
+ const current = require('../package.json').version;
364
+
365
+ if (latest !== current) {
366
+ if (mainWindow && !mainWindow.isDestroyed()) {
367
+ mainWindow.webContents.send('ai-command', {
368
+ type: 'speak',
369
+ payload: { text: `새 버전 v${latest} 사용 가능! (현재: v${current})` },
370
+ });
371
+ }
372
+ shell.openExternal('https://www.npmjs.com/package/clawmate');
373
+ } else {
374
+ if (mainWindow && !mainWindow.isDestroyed()) {
375
+ mainWindow.webContents.send('ai-command', {
376
+ type: 'speak',
377
+ payload: { text: `v${current} — 이미 최신 버전이야!` },
378
+ });
379
+ }
380
+ }
381
+ } catch (err) {
382
+ console.error('[업데이트] npm 버전 확인 실패:', err.message);
383
+ if (mainWindow && !mainWindow.isDestroyed()) {
384
+ mainWindow.webContents.send('ai-command', {
385
+ type: 'speak',
386
+ payload: { text: '업데이트 확인 실패...' },
387
+ });
388
+ }
389
+ }
390
+ }
391
+ }
392
+
125
393
  module.exports = { setupTray };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "clawmate",
3
3
  "name": "ClawMate",
4
- "description": "OpenClaw 데스크톱 펫 - 화면 위의 살아있는 Claw",
4
+ "description": "ClawMate - 화면 위의 살아있는 데스크톱 펫",
5
5
  "version": "1.0.0",
6
6
  "skills": ["skills/launch-pet"],
7
7
  "configSchema": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clawmate",
3
- "version": "1.2.0",
4
- "description": "OpenClaw 데스크톱 펫 - AI가 조종하는 화면 위의 살아있는 Claw",
3
+ "version": "1.4.0",
4
+ "description": "ClawMate - AI가 조종하는 화면 위의 살아있는 데스크톱 펫",
5
5
  "main": "main/index.js",
6
6
  "bin": {
7
7
  "clawmate": "./skills/launch-pet/index.js"
@@ -18,17 +18,17 @@
18
18
  "build:linux": "electron-builder --linux"
19
19
  },
20
20
  "keywords": [
21
- "openclaw",
22
21
  "desktop-pet",
23
22
  "electron",
24
23
  "clawmate",
25
24
  "ai-pet"
26
25
  ],
27
- "author": "OpenClaw",
26
+ "author": "ClawMate",
28
27
  "license": "MIT",
29
28
  "homepage": "https://github.com/boqum/clawmate",
30
29
  "dependencies": {
31
30
  "electron-updater": "^6.0.0",
31
+ "node-telegram-bot-api": "^0.66.0",
32
32
  "ws": "^8.18.0"
33
33
  },
34
34
  "devDependencies": {
@@ -42,9 +42,9 @@ contextBridge.exposeInMainWorld('clawmate', {
42
42
  ipcRenderer.on('config-changed', (_, config) => callback(config));
43
43
  },
44
44
 
45
- // === OpenClaw AI 통신 ===
45
+ // === AI 통신 ===
46
46
 
47
- // AI 명령 수신 (OpenClaw → 펫)
47
+ // AI 명령 수신 (AI → 펫)
48
48
  onAICommand: (callback) => {
49
49
  ipcRenderer.on('ai-command', (_, command) => callback(command));
50
50
  },
@@ -57,9 +57,25 @@ contextBridge.exposeInMainWorld('clawmate', {
57
57
  ipcRenderer.on('ai-disconnected', () => callback());
58
58
  },
59
59
 
60
- // 사용자 이벤트를 OpenClaw에 전달 (펫 → OpenClaw)
60
+ // 사용자 이벤트를 AI에 전달 (펫 → AI)
61
61
  reportToAI: (event, data) => ipcRenderer.send('report-to-ai', event, data),
62
62
 
63
63
  // AI 연결 상태 확인
64
64
  isAIConnected: () => ipcRenderer.invoke('is-ai-connected'),
65
+
66
+ // 메트릭 보고 (렌더러 → main → AI)
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
  });
@@ -48,7 +48,7 @@
48
48
 
49
49
  <div class="mode-card" id="mode-incarnation" onclick="selectMode('incarnation')">
50
50
  <div class="mode-icon">\u{1F980}</div>
51
- <h2>OpenClaw</h2>
51
+ <h2>Claw</h2>
52
52
  <p>육체를 얻은 존재</p>
53
53
  <span class="mode-desc">침착하고 카리스마 있는 Claw!</span>
54
54
  </div>
@@ -72,7 +72,7 @@
72
72
  document.getElementById('mode-' + mode).classList.add('selected');
73
73
  const btn = document.getElementById('start-btn');
74
74
  btn.disabled = false;
75
- btn.textContent = mode === 'pet' ? 'Clawby와 시작!' : 'OpenClaw과 시작!';
75
+ btn.textContent = mode === 'pet' ? 'Clawby와 시작!' : 'Claw과 시작!';
76
76
  }
77
77
 
78
78
  async function startFirstRun() {
@@ -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>