claude-pet 2.0.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.

Potentially problematic release.


This version of claude-pet might be problematic. Click here for more details.

Files changed (133) hide show
  1. package/.claude/commands/feed.md +28 -0
  2. package/.claude/commands/name.md +28 -0
  3. package/.claude/commands/pet.md +29 -0
  4. package/.claude/commands/play.md +29 -0
  5. package/.claude/settings.local.json +41 -0
  6. package/.github/workflows/AGENTS.md +60 -0
  7. package/.github/workflows/build.yml +87 -0
  8. package/AGENTS.md +66 -0
  9. package/LICENSE +15 -0
  10. package/README.md +292 -0
  11. package/bin/claude-pet.js +42 -0
  12. package/build/AGENTS.md +50 -0
  13. package/build/dmg-background.png +0 -0
  14. package/build/entitlements.mac.plist +14 -0
  15. package/build/icon.ico +0 -0
  16. package/build/icon.png +0 -0
  17. package/build/installerHeader.bmp +0 -0
  18. package/build/installerSidebar.bmp +0 -0
  19. package/build/tray-icon.png +0 -0
  20. package/dist/main/core/badge-manager.js +49 -0
  21. package/dist/main/core/badge-registry.js +72 -0
  22. package/dist/main/core/badge-triggers.js +45 -0
  23. package/dist/main/core/contextual-messages.js +372 -0
  24. package/dist/main/core/messages.js +440 -0
  25. package/dist/main/core/mood-engine.js +145 -0
  26. package/dist/main/core/pet-messages.js +612 -0
  27. package/dist/main/core/pet-state-engine.js +232 -0
  28. package/dist/main/core/quote-collection.js +60 -0
  29. package/dist/main/core/quote-registry.js +175 -0
  30. package/dist/main/core/quote-triggers.js +62 -0
  31. package/dist/main/core/usage-tracker.js +625 -0
  32. package/dist/main/main/auto-launch.js +39 -0
  33. package/dist/main/main/auto-updater.js +98 -0
  34. package/dist/main/main/event-watcher.js +174 -0
  35. package/dist/main/main/ipc-handlers.js +89 -0
  36. package/dist/main/main/main.js +422 -0
  37. package/dist/main/main/preload.js +93 -0
  38. package/dist/main/main/settings-window.js +49 -0
  39. package/dist/main/main/share-card.js +139 -0
  40. package/dist/main/main/skin-manager.js +118 -0
  41. package/dist/main/main/tray.js +88 -0
  42. package/dist/main/shared/i18n.js +392 -0
  43. package/dist/main/shared/types.js +25 -0
  44. package/dist/main/shared/utils.js +9 -0
  45. package/dist/renderer/assets/claude-pet.png +0 -0
  46. package/dist/renderer/assets/index-BMnMEuOf.js +9 -0
  47. package/dist/renderer/assets/index-qzlrlqpX.css +1 -0
  48. package/dist/renderer/index.html +30 -0
  49. package/dist/renderer/share-card-template/card.html +148 -0
  50. package/docs/AGENTS.md +42 -0
  51. package/docs/images/angry.png +0 -0
  52. package/docs/images/character.webp +0 -0
  53. package/docs/images/claude-mama.png +0 -0
  54. package/docs/images/happy.png +0 -0
  55. package/docs/images/proud.png +0 -0
  56. package/docs/images/share-card-example.png +0 -0
  57. package/docs/images/worried_1.png +0 -0
  58. package/docs/images/worried_2.png +0 -0
  59. package/docs/spritesheet-bugs.md +240 -0
  60. package/docs/superpowers/plans/2026-03-10-compact-widget.md +888 -0
  61. package/docs/superpowers/plans/2026-03-10-viral-features.md +1874 -0
  62. package/docs/superpowers/plans/2026-03-14-update-ux.md +362 -0
  63. package/docs/superpowers/plans/2026-03-14-v1.1-features.md +2139 -0
  64. package/docs/superpowers/specs/2026-03-10-compact-widget-design.md +150 -0
  65. package/docs/superpowers/specs/2026-03-10-viral-features-design.md +217 -0
  66. package/docs/superpowers/specs/2026-03-14-streak-calendar-design.md +26 -0
  67. package/docs/superpowers/specs/2026-03-14-update-ux-design.md +172 -0
  68. package/docs/superpowers/specs/2026-03-14-v1.1-features-design.md +342 -0
  69. package/electron-builder.yml +75 -0
  70. package/package.json +48 -0
  71. package/scripts/AGENTS.md +60 -0
  72. package/scripts/install.ps1 +47 -0
  73. package/scripts/install.sh +98 -0
  74. package/scripts/make-icon.js +119 -0
  75. package/scripts/notarize.js +18 -0
  76. package/src/AGENTS.md +47 -0
  77. package/src/core/AGENTS.md +58 -0
  78. package/src/core/__tests__/AGENTS.md +60 -0
  79. package/src/core/__tests__/badge-triggers.test.ts +83 -0
  80. package/src/core/__tests__/contextual-messages.test.ts +87 -0
  81. package/src/core/__tests__/pet-state-engine.test.ts +350 -0
  82. package/src/core/__tests__/quote-collection.test.ts +62 -0
  83. package/src/core/__tests__/quote-triggers.test.ts +110 -0
  84. package/src/core/badge-manager.ts +50 -0
  85. package/src/core/badge-registry.ts +71 -0
  86. package/src/core/badge-triggers.ts +41 -0
  87. package/src/core/contextual-messages.ts +381 -0
  88. package/src/core/pet-messages.ts +615 -0
  89. package/src/core/pet-state-engine.ts +272 -0
  90. package/src/core/quote-collection.ts +63 -0
  91. package/src/core/quote-registry.ts +181 -0
  92. package/src/core/quote-triggers.ts +64 -0
  93. package/src/core/usage-tracker.ts +680 -0
  94. package/src/main/AGENTS.md +70 -0
  95. package/src/main/auto-launch.ts +38 -0
  96. package/src/main/auto-updater.ts +106 -0
  97. package/src/main/event-watcher.ts +159 -0
  98. package/src/main/ipc-handlers.ts +107 -0
  99. package/src/main/main.ts +425 -0
  100. package/src/main/preload.ts +111 -0
  101. package/src/main/settings-window.ts +50 -0
  102. package/src/main/share-card.ts +153 -0
  103. package/src/main/skin-manager.ts +119 -0
  104. package/src/main/tray.ts +94 -0
  105. package/src/renderer/AGENTS.md +62 -0
  106. package/src/renderer/App.tsx +270 -0
  107. package/src/renderer/assets/claude-mama.png +0 -0
  108. package/src/renderer/assets/claude-pet.png +0 -0
  109. package/src/renderer/components/AGENTS.md +50 -0
  110. package/src/renderer/components/Character.tsx +327 -0
  111. package/src/renderer/components/SpeechBubble.tsx +182 -0
  112. package/src/renderer/components/UsageIndicator.tsx +268 -0
  113. package/src/renderer/electron.d.ts +34 -0
  114. package/src/renderer/hooks/AGENTS.md +55 -0
  115. package/src/renderer/hooks/usePetState.ts +59 -0
  116. package/src/renderer/hooks/useWidgetMode.ts +18 -0
  117. package/src/renderer/index.html +29 -0
  118. package/src/renderer/main.tsx +13 -0
  119. package/src/renderer/pages/AGENTS.md +53 -0
  120. package/src/renderer/pages/Collection.tsx +252 -0
  121. package/src/renderer/pages/Settings.tsx +815 -0
  122. package/src/renderer/share-card-template/card.html +148 -0
  123. package/src/renderer/styles/AGENTS.md +50 -0
  124. package/src/renderer/styles/styles.css +166 -0
  125. package/src/shared/AGENTS.md +48 -0
  126. package/src/shared/i18n.ts +395 -0
  127. package/src/shared/types.ts +163 -0
  128. package/src/shared/utils.ts +6 -0
  129. package/tsconfig.json +16 -0
  130. package/tsconfig.main.json +12 -0
  131. package/tsconfig.renderer.json +12 -0
  132. package/vite.config.ts +47 -0
  133. package/vitest.config.ts +9 -0
@@ -0,0 +1,342 @@
1
+ # v1.1 Features Design Spec
2
+
3
+ **Date:** 2026-03-14
4
+ **Features:** Always on Top, Contextual Messages, Achievement Badges, Custom Character Skins
5
+
6
+ ---
7
+
8
+ ## Feature A: Always on Top
9
+
10
+ ### Overview
11
+ 데스크톱 마스코트 특성상 항상 다른 윈도우 위에 표시되어야 하므로, `alwaysOnTop` 설정을 추가한다.
12
+
13
+ ### Design
14
+ - `MamaSettings`에 `alwaysOnTop: boolean` 추가 (기본값 `true`)
15
+ - `createWindow()`에서 기존 하드코딩된 `alwaysOnTop: true` (main.ts:115)를 `alwaysOnTop: store.get('alwaysOnTop', true)`로 교체
16
+ - `ipc-handlers.ts`의 `SETTINGS_SET` 핸들러에서 `alwaysOnTop` 변경 시 `mainWindow.setAlwaysOnTop(value)` 호출 (mainWindow 파라미터는 `registerIpcHandlers()`에서 이미 전달됨)
17
+ - `ipc-handlers.ts`의 store `defaults`에 `alwaysOnTop: true` 추가
18
+ - Settings 페이지에 토글 UI 추가
19
+ - 트레이 메뉴에 체크박스 토글 추가 (빠른 접근)
20
+
21
+ ### Files to Modify
22
+ | File | Change |
23
+ |------|--------|
24
+ | `src/shared/types.ts` | `MamaSettings`에 `alwaysOnTop` 필드 추가 |
25
+ | `src/main/main.ts` | `createWindow()`에 `alwaysOnTop` 옵션, 설정 변경 리스너 |
26
+ | `src/main/ipc-handlers.ts` | `SETTINGS_SET`에서 `alwaysOnTop` 변경 시 `setAlwaysOnTop()` 호출 |
27
+ | `src/main/tray.ts` | 트레이 메뉴에 "Always on Top" 체크박스 추가 |
28
+ | `src/renderer/pages/Settings.tsx` | 토글 UI 추가 |
29
+ | `src/shared/i18n.ts` | "항상 위에" 번역 문자열 |
30
+
31
+ ---
32
+
33
+ ## Feature B: Contextual Messages (상황 인식 메시지)
34
+
35
+ ### Overview
36
+ 기존 무드 기반 메시지에 상황 인식을 추가하여 더 자연스러운 메시지를 보여준다. 상황 조건이 매칭되면 상황 메시지를 우선 표시하고, 아니면 기존 무드 메시지로 fallback한다.
37
+
38
+ ### Contextual Triggers
39
+
40
+ | 상황 | 조건 | 적용 무드 |
41
+ |------|------|----------|
42
+ | **요일 (주말)** | `now.getDay()` === 0 or 6 | 모든 무드 |
43
+ | **연속 미사용** | `dailyHistory`에서 N일 연속 percent === 0 (N >= 2) | angry, worried |
44
+ | **급증** | 전일 대비 30%+ 증가 | happy, proud |
45
+ | **리셋 임박** | 리셋까지 3시간 이내 + 주간 50% 미만 | worried, angry |
46
+
47
+ ### Message Selection Logic
48
+
49
+ ```
50
+ getContextualMessage(mood, locale, context):
51
+ 1. Evaluate context triggers in priority order:
52
+ - 연속 미사용 (highest - most urgent)
53
+ - 리셋 임박
54
+ - 급증
55
+ - 요일 (lowest - most general)
56
+ 2. If trigger matches AND has messages for current mood:
57
+ return contextual message (time-seeded rotation within context pool)
58
+ 3. Else:
59
+ return getMessage(mood, locale) // existing fallback
60
+ ```
61
+
62
+ ### Context Message Pools
63
+
64
+ Contextual messages only apply to `MamaMood` types (angry, worried, happy, proud) — not to error expressions (confused, sleeping) or special states (fiveHourWarning, rateLimited). The `MoodKey` type in `messages.ts` is a local alias and should NOT be reused here.
65
+
66
+ ```typescript
67
+ const CONTEXTUAL_POOLS: Record<ContextTrigger, Partial<Record<MamaMood, Record<Locale, string[]>>>> = {
68
+ weekend: {
69
+ angry: {
70
+ ko: ["주말인데 토큰 안 쓰면 월요일에 후회한다?", ...],
71
+ en: ["Not using tokens on a weekend? Monday-you will regret it!", ...],
72
+ ja: [...],
73
+ zh: [...],
74
+ },
75
+ happy: {
76
+ ko: ["주말에도 열심히 하네~ 역시 내 자식!", ...],
77
+ ...
78
+ },
79
+ // ... other moods
80
+ },
81
+ unusedStreak: { ... },
82
+ spike: { ... },
83
+ resetImminent: { ... },
84
+ };
85
+ ```
86
+
87
+ ### Required Context Extensions
88
+
89
+ 기존 `TriggerContext`에 `resetsAt` 필드를 추가하여 확장. 별도 `MessageContext`를 만들지 않고 `TriggerContext`를 재사용하여 Feature C의 `BadgeTriggerContext extends TriggerContext`와도 일관성 유지.
90
+
91
+ ```typescript
92
+ // types.ts — TriggerContext에 추가
93
+ interface TriggerContext {
94
+ // ... existing fields ...
95
+ resetsAt: string | null; // ISO timestamp — for resetImminent trigger
96
+ }
97
+ ```
98
+
99
+ **Integration with `computeMood` pipeline:** 현재 `main.ts`에서 `getMessage(mood, locale)`을 직접 호출하는 부분을 `getContextualMessage(mood, locale, triggerContext)`로 교체. `computeMood()` 자체는 수정하지 않음 — mood 계산과 메시지 선택을 분리.
100
+
101
+ **`dailyHistory` 제약:** `dailyHistory`는 최근 14일로 제한됨 (`main.ts`에서 `slice(-14)`). 따라서 `unusedStreak`은 최대 14일까지만 감지 가능.
102
+
103
+ ### Message Count Target
104
+
105
+ 각 트리거당 무드당 5개 이상의 메시지 (4개 언어 × 4개 트리거 × 해당 무드 × 5개 = 상당량).
106
+ 초기에는 트리거당 주요 무드 2~3개만 커버하고 점진적으로 확장.
107
+
108
+ | Trigger | Covered Moods | Messages per mood per locale | Total (4 lang) |
109
+ |---------|--------------|------------------------------|----------------|
110
+ | weekend | angry, worried, happy, proud | 5 | 80 |
111
+ | unusedStreak | angry, worried | 5 | 40 |
112
+ | spike | happy, proud | 5 | 40 |
113
+ | resetImminent | angry, worried | 5 | 40 |
114
+ | **Total** | | | **200** |
115
+
116
+ ### Files to Create/Modify
117
+ | File | Change |
118
+ |------|--------|
119
+ | `src/core/contextual-messages.ts` | **New** — context trigger evaluation + message pools |
120
+ | `src/core/messages.ts` | Export helper, keep existing pools unchanged |
121
+ | `src/shared/types.ts` | `MessageContext` interface, `ContextTrigger` type |
122
+ | `src/main/main.ts` | Pass context to message selection |
123
+ | `src/core/__tests__/contextual-messages.test.ts` | **New** — trigger logic tests |
124
+
125
+ ---
126
+
127
+ ## Feature C: Achievement Badges
128
+
129
+ ### Overview
130
+ 사용량 마일스톤, 연속 사용, 엄마 관계 기반의 업적 배지 시스템. 기존 Collection(도감) 페이지에 탭으로 추가.
131
+
132
+ ### Badge Tiers
133
+
134
+ | Tier | Color | 난이도 |
135
+ |------|-------|--------|
136
+ | Bronze | #CD7F32 | 쉬움 — 기본 사용으로 달성 |
137
+ | Silver | #C0C0C0 | 보통 — 꾸준한 사용 필요 |
138
+ | Gold | #FFD700 | 어려움 — 장기 헌신 필요 |
139
+
140
+ ### Badge Registry
141
+
142
+ | ID | Tier | Name (ko) | Name (en) | Condition |
143
+ |----|------|-----------|-----------|-----------|
144
+ | `badge_first_call` | Bronze | 첫 걸음 | First Steps | 첫 API 호출 (`firstApiCallSeen` transition) |
145
+ | `badge_streak_3` | Bronze | 3일 연속 | 3-Day Streak | dailyHistory 3일 연속 percent > 0 |
146
+ | `badge_mama_7days` | Bronze | 엄마와 7일 | 7 Days with Mom | 설치 후 7일 경과 |
147
+ | `badge_half` | Silver | 반타작 | Halfway There | 주간 50% 달성 |
148
+ | `badge_streak_7` | Silver | 7일 연속 | 7-Day Streak | dailyHistory 7일 연속 percent > 0 |
149
+ | `badge_proud_10` | Silver | 자랑스러운 아들 | Mom's Pride | proud 상태 누적 10회 |
150
+ | `badge_full` | Gold | 풀 가동 | Full Power | 주간 100% 달성 |
151
+ | `badge_streak_30` | Gold | 30일 연속 | 30-Day Streak | dailyHistory 30일 연속 percent > 0 |
152
+ | `badge_survivor` | Gold | 생존왕 | Survivor | angry 상태 누적 10회 후에도 계속 사용 |
153
+
154
+ ### Architecture
155
+
156
+ **QuoteCollectionManager 패턴을 따름:**
157
+
158
+ ```typescript
159
+ interface BadgeEntry {
160
+ id: string;
161
+ tier: 'bronze' | 'silver' | 'gold';
162
+ name: Record<Locale, string>;
163
+ description: Record<Locale, string>;
164
+ icon: string; // emoji or icon identifier
165
+ }
166
+
167
+ interface UnlockedBadge {
168
+ id: string;
169
+ unlockedAt: string; // ISO
170
+ }
171
+
172
+ interface BadgeState {
173
+ unlocked: UnlockedBadge[];
174
+ totalCount: number;
175
+ byTier: Record<BadgeTier, { unlocked: number; total: number }>;
176
+ }
177
+ ```
178
+
179
+ **Pure functions:**
180
+ - `badge-registry.ts` — badge definitions
181
+ - `badge-triggers.ts` — `evaluateBadgeTriggers(ctx): string[]`
182
+ - `badge-manager.ts` — unlock state management
183
+
184
+ **Persistence context extensions:**
185
+
186
+ 배지 트리거 평가를 위해 추가 통계가 필요:
187
+
188
+ ```typescript
189
+ interface BadgeTriggerContext extends TriggerContext {
190
+ proudCount: number; // 누적 proud 상태 횟수
191
+ angryCount: number; // 누적 angry 상태 횟수
192
+ }
193
+ ```
194
+
195
+ `electron-store`에 저장 (기존 `(store as any).set()` 패턴 사용):
196
+ - `unlockedBadges: UnlockedBadge[]`
197
+ - `moodCounts: Record<MamaMood, number>` — 무드별 누적 횟수
198
+
199
+ **`moodCounts` 증가 시점:** 무드가 이전 poll과 다를 때만 증가 (매 poll tick이 아님). `main.ts`의 polling loop에서 이전 무드를 추적하고, 무드 전환 시에만 카운트.
200
+
201
+ **`badge_survivor` 조건 명확화:** `angryCount >= 10`이고 현재 `weeklyUtilization > 0`이면 해금. 즉, angry를 10번 이상 경험한 뒤에도 여전히 사용 중이면 달성.
202
+
203
+ ### UI
204
+
205
+ - Collection 페이지 내부에 "명언 | 배지" 탭 추가 (기존 Settings의 `settings | collection` 탭 구조 아래 중첩 탭)
206
+ - 배지 그리드: 아이콘 + 이름 + 설명
207
+ - 미해금: 실루엣(grayscale) + "???" 표시
208
+ - 해금 시: `BADGE_UNLOCKED` IPC 채널로 renderer에 통지 → 토스트 알림 (SpeechBubble 스타일 재활용)
209
+ - 티어별 색상 border/glow
210
+
211
+ ### Files to Create/Modify
212
+
213
+ | File | Change |
214
+ |------|--------|
215
+ | `src/shared/types.ts` | `BadgeEntry`, `UnlockedBadge`, `BadgeState`, `BadgeTier`, `BadgeTriggerContext`, IPC channels: `BADGE_GET`, `BADGE_UNLOCKED` |
216
+ | `src/core/badge-registry.ts` | **New** — badge definitions (9 badges) |
217
+ | `src/core/badge-triggers.ts` | **New** — `evaluateBadgeTriggers()` pure function |
218
+ | `src/core/badge-manager.ts` | **New** — `BadgeManager` class |
219
+ | `src/core/__tests__/badge-triggers.test.ts` | **New** — trigger tests |
220
+ | `src/main/main.ts` | Badge evaluation in polling loop, mood count tracking |
221
+ | `src/main/ipc-handlers.ts` | Badge IPC handlers |
222
+ | `src/main/preload.ts` | Badge API exposure |
223
+ | `src/renderer/pages/Collection.tsx` | 탭 UI, badge grid |
224
+ | `src/renderer/electron.d.ts` | Badge API types |
225
+ | `src/shared/i18n.ts` | Badge UI strings |
226
+
227
+ ---
228
+
229
+ ## Feature D: Custom Character Skins
230
+
231
+ ### Overview
232
+ 유저가 자신만의 캐릭터 이미지를 업로드하여 사용. 4가지 모드 지원 (default 포함).
233
+
234
+ ### Skin Modes
235
+
236
+ | Mode | Input | Behavior |
237
+ |------|-------|----------|
238
+ | `default` | 없음 | 기본 claude-mama.png 사용 |
239
+ | `single` | PNG 1장 | 전 무드 동일 이미지, MoodOverlay 이펙트 유지 |
240
+ | `per-mood` | PNG 최대 6장 | 무드별 이미지, MoodOverlay 유지. 누락된 무드는 기본 claude-mama.png fallback |
241
+ | `spritesheet` | PNG 1장 + grid config | row/column 그리드로 무드별 프레임 지정. `frameW = naturalWidth / columns`, `frameH = naturalHeight / rows` |
242
+
243
+ ### File Validation
244
+ - 최대 파일 크기: 2MB
245
+ - 최대 이미지 크기: 512x512px (프레임 기준)
246
+ - PNG/JPG/GIF만 허용
247
+ - 유효하지 않은 파일 업로드 시 에러 메시지 표시
248
+
249
+ ### Settings Data Structure
250
+
251
+ ```typescript
252
+ type SkinMode = 'default' | 'single' | 'per-mood' | 'spritesheet';
253
+ type Expression = MamaMood | MamaErrorExpression;
254
+
255
+ interface SkinConfig {
256
+ mode: SkinMode;
257
+ // single mode
258
+ singleImagePath?: string;
259
+ // per-mood mode
260
+ moodImages?: Partial<Record<Expression, string>>;
261
+ // spritesheet mode
262
+ spritesheet?: {
263
+ imagePath: string;
264
+ columns: number;
265
+ rows: number;
266
+ moodMap: Record<Expression, { col: number; row: number }>;
267
+ };
268
+ }
269
+ ```
270
+
271
+ `MamaSettings`에 `skin?: SkinConfig` 추가. 기본값은 `{ mode: 'default' }`.
272
+
273
+ ### File Storage
274
+
275
+ - 업로드 이미지를 `app.getPath('userData')/skins/` 에 복사
276
+ - 파일명: `skin-{timestamp}.png`, `skin-{mood}-{timestamp}.png`
277
+ - 설정에는 경로만 저장
278
+ - **Cleanup:** 새 스킨 업로드 시 이전 스킨 파일 삭제 (orphan 방지). `skin-manager.ts`에서 관리
279
+
280
+ ### IPC
281
+
282
+ | Channel | Direction | Purpose |
283
+ |---------|-----------|---------|
284
+ | `mama:upload-skin` | renderer → main | 파일 선택 다이얼로그 + 복사 |
285
+ | `mama:reset-skin` | renderer → main | 기본 스킨으로 리셋 |
286
+ | `mama:get-skin-config` | renderer → main | 현재 스킨 설정 조회 |
287
+
288
+ ### Character.tsx Changes
289
+
290
+ ```
291
+ 기존: mamaPng (하드코딩) → imgStyle에 적용
292
+ 변경: skinSrc를 props 또는 IPC로 받아서 적용
293
+
294
+ single: src = singleImagePath
295
+ per-mood: src = moodImages[expression] ?? fallback
296
+ spritesheet: src = imagePath, objectPosition으로 프레임 선택
297
+ - objectFit: 'none'
298
+ - objectPosition: `-${col * frameW}px -${row * frameH}px`
299
+ - width/height: frameW/frameH
300
+ ```
301
+
302
+ ### Settings UI
303
+
304
+ Settings 페이지에 "캐릭터" 섹션:
305
+
306
+ 1. **모드 선택**: 라디오 (기본 / 단일 이미지 / 무드별 / 스프라이트 시트)
307
+ 2. **단일 이미지**: 파일 업로드 버튼 + 미리보기
308
+ 3. **무드별**: 6개 업로드 슬롯 (angry, worried, happy, proud, confused, sleeping) + 각각 미리보기
309
+ 4. **스프라이트 시트**: 파일 업로드 + columns/rows 숫자 입력 + 무드별 (col, row) 매핑
310
+ 5. **기본으로 되돌리기** 버튼
311
+
312
+ ### Files to Create/Modify
313
+
314
+ | File | Change |
315
+ |------|--------|
316
+ | `src/shared/types.ts` | `SkinConfig`, `SkinMode` types, IPC channels |
317
+ | `src/main/skin-manager.ts` | **New** — 파일 복사, 경로 관리 |
318
+ | `src/main/ipc-handlers.ts` | Skin upload/reset IPC handlers |
319
+ | `src/main/preload.ts` | Skin API exposure |
320
+ | `src/renderer/components/Character.tsx` | skinSrc 로직, spritesheet 렌더링 |
321
+ | `src/renderer/pages/Settings.tsx` | 캐릭터 섹션 UI |
322
+ | `src/renderer/electron.d.ts` | Skin API types |
323
+ | `src/shared/i18n.ts` | Skin UI strings |
324
+
325
+ ---
326
+
327
+ ## Implementation Order
328
+
329
+ ```
330
+ A (Always on Top) → B (Contextual Messages) → C (Badges) → D (Skins)
331
+ ```
332
+
333
+ 각 feature는 독립적이며 순차적으로 구현 가능. B와 C는 TriggerContext를 공유하므로 B에서 확장한 context를 C에서 재활용.
334
+
335
+ ## Testing Strategy
336
+
337
+ | Feature | Test Type |
338
+ |---------|-----------|
339
+ | A | 수동 테스트 (Electron window 동작) |
340
+ | B | Unit test: contextual trigger logic, message selection fallback |
341
+ | C | Unit test: badge trigger evaluation, badge manager unlock logic |
342
+ | D | 수동 테스트 (파일 업로드, 렌더링) |
@@ -0,0 +1,75 @@
1
+ appId: com.claudepet.app
2
+ productName: Claude Pet
3
+ directories:
4
+ output: release
5
+ files:
6
+ - dist/**/*
7
+ - package.json
8
+ - "!dist/renderer/**/*.map"
9
+ asar: true
10
+ publish:
11
+ provider: github
12
+ owner: scm1400
13
+ repo: claude-pet
14
+ win:
15
+ target:
16
+ - target: nsis
17
+ arch:
18
+ - x64
19
+ icon: build/icon.ico
20
+ mac:
21
+ target:
22
+ - target: dmg
23
+ arch:
24
+ - x64
25
+ - arm64
26
+ - target: zip
27
+ arch:
28
+ - x64
29
+ - arm64
30
+ icon: build/icon.png
31
+ category: public.app-category.utilities
32
+ hardenedRuntime: true
33
+ gatekeeperAssess: true
34
+ entitlements: build/entitlements.mac.plist
35
+ entitlementsInherit: build/entitlements.mac.plist
36
+ notarize: false
37
+ #afterSign: scripts/notarize.js
38
+ linux:
39
+ target:
40
+ - target: AppImage
41
+ arch:
42
+ - x64
43
+ - target: deb
44
+ arch:
45
+ - x64
46
+ icon: build/icon.png
47
+ category: Utility
48
+ nsis:
49
+ oneClick: true
50
+ allowToChangeInstallationDirectory: false
51
+ perMachine: false
52
+ createDesktopShortcut: true
53
+ createStartMenuShortcut: true
54
+ installerSidebar: build/installerSidebar.bmp
55
+ uninstallerSidebar: build/installerSidebar.bmp
56
+ installerHeader: build/installerHeader.bmp
57
+ dmg:
58
+ writeUpdateInfo: false
59
+ background: build/dmg-background.png
60
+ window:
61
+ width: 540
62
+ height: 380
63
+ contents:
64
+ - x: 140
65
+ y: 250
66
+ type: file
67
+ - x: 400
68
+ y: 250
69
+ type: link
70
+ path: /Applications
71
+ extraResources:
72
+ - from: build/tray-icon.png
73
+ to: tray-icon.png
74
+ extraMetadata:
75
+ main: dist/main/main/main.js
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "claude-pet",
3
+ "version": "2.0.0",
4
+ "description": "Claude Pet - virtual pet coding companion desktop widget",
5
+ "main": "dist/main/main/main.js",
6
+ "bin": {
7
+ "claude-pet": "bin/claude-pet.js"
8
+ },
9
+ "scripts": {
10
+ "prepare": "npm run build",
11
+ "dev": "npm run build:main && concurrently -k -n VITE,ELECTRON -c cyan,yellow \"vite\" \"cross-env NODE_ENV=development electron dist/main/main/main.js\"",
12
+ "build:renderer": "vite build",
13
+ "build:main": "tsc -p tsconfig.main.json",
14
+ "build": "npm run build:renderer && npm run build:main",
15
+ "start": "electron dist/main/main/main.js",
16
+ "test": "vitest run",
17
+ "make-icon": "node scripts/make-icon.js",
18
+ "build:win": "electron-builder --win",
19
+ "build:mac": "electron-builder --mac",
20
+ "build:all": "electron-builder --win --mac"
21
+ },
22
+ "keywords": [],
23
+ "author": "scm1400",
24
+ "license": "ISC",
25
+ "type": "commonjs",
26
+ "devDependencies": {
27
+ "@types/react": "^19.2.14",
28
+ "@types/react-dom": "^19.2.3",
29
+ "@vitejs/plugin-react": "^5.1.4",
30
+ "concurrently": "^9.2.1",
31
+ "cross-env": "^10.1.0",
32
+ "electron-builder": "^26.8.1",
33
+ "png-to-ico": "^3.0.1",
34
+ "sharp": "^0.34.5",
35
+ "tsx": "^4.21.0",
36
+ "typescript": "^5.9.3",
37
+ "vite": "^7.3.1",
38
+ "vitest": "^4.0.18"
39
+ },
40
+ "dependencies": {
41
+ "auto-launch": "^5.0.6",
42
+ "electron": "^40.8.0",
43
+ "electron-store": "8.2.0",
44
+ "electron-updater": "^6.8.3",
45
+ "react": "^19.2.4",
46
+ "react-dom": "^19.2.4"
47
+ }
48
+ }
@@ -0,0 +1,60 @@
1
+ <!-- Parent: ../AGENTS.md -->
2
+ <!-- Generated: 2026-03-14 -->
3
+
4
+ # scripts/
5
+
6
+ ## Purpose
7
+ Build-time utility scripts that run outside the Electron app. `make-icon.js` generates all platform-specific icon and installer image assets from the source character artwork. `notarize.js` is an electron-builder afterSign hook that submits the packaged macOS `.app` to Apple's notarization service.
8
+
9
+ ## Key Files
10
+ | File | Description |
11
+ |------|-------------|
12
+ | `make-icon.js` | Reads `docs/images/character.webp`, uses `sharp` to resize and composite, and writes: `build/icon.png` (512×512, macOS), `build/tray-icon.png` (32×32, runtime tray), `build/icon.ico` (256×256 via `png-to-ico`, Windows), `build/installerSidebar.bmp` (164×314, NSIS sidebar), `build/installerHeader.bmp` (150×57, NSIS header), `build/dmg-background.png` (540×380, macOS DMG). Run with `npm run make-icon`. |
13
+ | `notarize.js` | electron-builder `afterSign` hook. Calls `@electron/notarize` with `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, and `APPLE_TEAM_ID` environment variables. No-ops on non-macOS platforms. Invoked automatically by electron-builder during `npm run build:mac`. |
14
+
15
+ ## Subdirectories
16
+ | Directory | Purpose |
17
+ |-----------|---------|
18
+ | *(none)* | — |
19
+
20
+ ## For AI Agents
21
+
22
+ ### Working In This Directory
23
+ - `make-icon.js` uses CommonJS `require()` — it is a plain Node script, not compiled TypeScript.
24
+ - The source artwork must be at `docs/images/character.webp`. If the character art changes, re-run `npm run make-icon` to regenerate all build assets.
25
+ - `notarize.js` requires three environment variables at build time: `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`. These are set as GitHub Actions secrets in `.github/workflows/build.yml`.
26
+ - Do not run `notarize.js` manually — it is invoked by electron-builder's `afterSign` hook configured in `electron-builder.yml`.
27
+ - The NSIS installer images (`.bmp`) must be exact pixel dimensions: sidebar 164×314, header 150×57. `sharp` enforces this via `removeAlpha()` + BMP output.
28
+ - The DMG background (540×380 PNG) must have a transparent-safe format — `sharp` outputs PNG here (not BMP).
29
+
30
+ ### Testing Requirements
31
+ - Run `npm run make-icon` and verify all six output files appear in `build/` with correct dimensions using any image viewer.
32
+ - `notarize.js` is tested implicitly by the macOS CI job in `.github/workflows/build.yml`.
33
+
34
+ ### Common Patterns
35
+ - Sharp pipeline pattern used throughout `make-icon.js`:
36
+ ```js
37
+ await sharp(src)
38
+ .resize(W, H, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
39
+ .png()
40
+ .toFile(outPath);
41
+ ```
42
+ - Composite (SVG background + character image):
43
+ ```js
44
+ await sharp(Buffer.from(svgString))
45
+ .composite([{ input: charBuf, left: x, top: y }])
46
+ .removeAlpha()
47
+ .toFile(outPath);
48
+ ```
49
+
50
+ ## Dependencies
51
+ ### Internal
52
+ - Reads from `docs/images/character.webp`
53
+ - Writes to `build/`
54
+
55
+ ### External
56
+ - `sharp` ^0.34 — image processing
57
+ - `png-to-ico` ^3 — PNG-to-ICO conversion
58
+ - `@electron/notarize` — Apple notarization (consumed by `notarize.js`)
59
+
60
+ <!-- MANUAL: -->
@@ -0,0 +1,47 @@
1
+ # Claude Pet Installer for Windows
2
+ # Usage: irm https://raw.githubusercontent.com/scm1400/claude-pet/main/scripts/install.ps1 | iex
3
+ $ErrorActionPreference = "Stop"
4
+
5
+ $Repo = "scm1400/claude-pet"
6
+ $ApiUrl = "https://api.github.com/repos/$Repo/releases/latest"
7
+
8
+ function Info($msg) { Write-Host "→ $msg" -ForegroundColor Blue }
9
+ function Ok($msg) { Write-Host "✓ $msg" -ForegroundColor Green }
10
+ function Err($msg) { Write-Host "✗ $msg" -ForegroundColor Red; exit 1 }
11
+
12
+ Info "Fetching latest release..."
13
+ try {
14
+ $release = Invoke-RestMethod -Uri $ApiUrl -Headers @{ "User-Agent" = "claude-pet-installer" }
15
+ } catch {
16
+ Err "Failed to fetch release info: $_"
17
+ }
18
+
19
+ $version = $release.tag_name
20
+ Info "Latest version: $version"
21
+
22
+ # Find .exe installer asset
23
+ $asset = $release.assets | Where-Object { $_.name -match '\.exe$' } | Select-Object -First 1
24
+ if (-not $asset) {
25
+ Err "No Windows installer found in release $version"
26
+ }
27
+
28
+ $downloadUrl = $asset.browser_download_url
29
+ $fileName = $asset.name
30
+ $tempPath = Join-Path $env:TEMP $fileName
31
+
32
+ Info "Downloading $fileName..."
33
+ try {
34
+ $ProgressPreference = 'SilentlyContinue'
35
+ Invoke-WebRequest -Uri $downloadUrl -OutFile $tempPath -UseBasicParsing
36
+ } catch {
37
+ Err "Download failed: $_"
38
+ }
39
+ Ok "Downloaded to $tempPath"
40
+
41
+ Info "Running installer..."
42
+ Start-Process -FilePath $tempPath -Wait
43
+ Ok "Installation complete!"
44
+
45
+ # Cleanup
46
+ Remove-Item -Path $tempPath -ErrorAction SilentlyContinue
47
+ Info "Claude Pet should now be available in your Start Menu."
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Claude Pet Installer — downloads the latest release from GitHub
5
+ REPO="scm1400/claude-pet"
6
+ API_URL="https://api.github.com/repos/$REPO/releases/latest"
7
+
8
+ info() { printf "\033[1;34m→\033[0m %s\n" "$*"; }
9
+ ok() { printf "\033[1;32m✓\033[0m %s\n" "$*"; }
10
+ err() { printf "\033[1;31m✗\033[0m %s\n" "$*" >&2; exit 1; }
11
+
12
+ # Detect OS and architecture
13
+ OS="$(uname -s)"
14
+ ARCH="$(uname -m)"
15
+
16
+ case "$OS" in
17
+ Darwin) PLATFORM="mac" ;;
18
+ Linux) PLATFORM="linux" ;;
19
+ *) err "Unsupported OS: $OS. Use install.ps1 for Windows." ;;
20
+ esac
21
+
22
+ case "$ARCH" in
23
+ x86_64|amd64) ARCH_LABEL="x64" ;;
24
+ arm64|aarch64) ARCH_LABEL="arm64" ;;
25
+ *) err "Unsupported architecture: $ARCH" ;;
26
+ esac
27
+
28
+ info "Detected: $OS ($ARCH_LABEL)"
29
+
30
+ # Fetch latest release info
31
+ info "Fetching latest release..."
32
+ RELEASE_JSON=$(curl -fsSL "$API_URL") || err "Failed to fetch release info"
33
+ VERSION=$(echo "$RELEASE_JSON" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"//;s/".*//')
34
+ info "Latest version: $VERSION"
35
+
36
+ # Determine download asset
37
+ if [ "$PLATFORM" = "mac" ]; then
38
+ if [ "$ARCH_LABEL" = "arm64" ]; then
39
+ PATTERN="arm64.*\.dmg"
40
+ else
41
+ PATTERN="x64.*\.dmg\|Claude.Pet.*\.dmg"
42
+ fi
43
+ ASSET_URL=$(echo "$RELEASE_JSON" | grep '"browser_download_url"' | grep -i '\.dmg"' | head -1 | sed 's/.*"browser_download_url": *"//;s/".*//')
44
+ FILENAME="Claude-Pet-${VERSION}.dmg"
45
+ elif [ "$PLATFORM" = "linux" ]; then
46
+ # Prefer AppImage
47
+ ASSET_URL=$(echo "$RELEASE_JSON" | grep '"browser_download_url"' | grep -i '\.AppImage"' | head -1 | sed 's/.*"browser_download_url": *"//;s/".*//')
48
+ if [ -z "$ASSET_URL" ]; then
49
+ ASSET_URL=$(echo "$RELEASE_JSON" | grep '"browser_download_url"' | grep -i '\.deb"' | head -1 | sed 's/.*"browser_download_url": *"//;s/".*//')
50
+ FILENAME="claude-pet-${VERSION}.deb"
51
+ else
52
+ FILENAME="Claude-Pet-${VERSION}.AppImage"
53
+ fi
54
+ fi
55
+
56
+ [ -z "${ASSET_URL:-}" ] && err "No installer found for $PLATFORM ($ARCH_LABEL)"
57
+
58
+ TMPDIR="${TMPDIR:-/tmp}"
59
+ DOWNLOAD_PATH="$TMPDIR/$FILENAME"
60
+
61
+ info "Downloading $FILENAME..."
62
+ curl -fSL --progress-bar -o "$DOWNLOAD_PATH" "$ASSET_URL" || err "Download failed"
63
+ ok "Downloaded to $DOWNLOAD_PATH"
64
+
65
+ # Install
66
+ if [ "$PLATFORM" = "mac" ]; then
67
+ info "Mounting DMG..."
68
+ MOUNT_POINT=$(hdiutil attach "$DOWNLOAD_PATH" -nobrowse | tail -1 | awk '{print $NF}')
69
+ APP_PATH=$(find "$MOUNT_POINT" -name "*.app" -maxdepth 1 | head -1)
70
+ if [ -n "$APP_PATH" ]; then
71
+ info "Installing to /Applications..."
72
+ cp -R "$APP_PATH" /Applications/
73
+ hdiutil detach "$MOUNT_POINT" -quiet
74
+ ok "Claude Pet installed to /Applications!"
75
+ info "Run: open '/Applications/Claude Pet.app'"
76
+ else
77
+ hdiutil detach "$MOUNT_POINT" -quiet
78
+ err "No .app found in DMG"
79
+ fi
80
+ elif [ "$PLATFORM" = "linux" ]; then
81
+ if [[ "$FILENAME" == *.AppImage ]]; then
82
+ INSTALL_DIR="${HOME}/.local/bin"
83
+ mkdir -p "$INSTALL_DIR"
84
+ mv "$DOWNLOAD_PATH" "$INSTALL_DIR/claude-pet"
85
+ chmod +x "$INSTALL_DIR/claude-pet"
86
+ ok "Claude Pet installed to $INSTALL_DIR/claude-pet"
87
+ info "Make sure $INSTALL_DIR is in your PATH"
88
+ info "Run: claude-pet"
89
+ elif [[ "$FILENAME" == *.deb ]]; then
90
+ info "Installing .deb package..."
91
+ sudo dpkg -i "$DOWNLOAD_PATH" || sudo apt-get install -f -y
92
+ ok "Claude Pet installed!"
93
+ info "Run: claude-pet"
94
+ fi
95
+ fi
96
+
97
+ rm -f "$DOWNLOAD_PATH" 2>/dev/null || true
98
+ ok "Installation complete!"