clauddy 1.6.0 → 1.7.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/README.md CHANGED
@@ -115,7 +115,8 @@ Settings saved from the UI live in `~/.claude-usage-monitor/config.json`, so you
115
115
  ```jsonc
116
116
  {
117
117
  "alerts": true, // macOS notifications on/off
118
- "alertThresholds": [80, 95], // notify when session/week cross these %
118
+ "alertThresholds": [80, 95], // notify when session/week cross these % (two levels)
119
+ "fireThreshold": 90, // session % at which the pet catches fire (maxed out stays 100)
119
120
  "pollIntervalMs": 4000, // how often local logs are re-read
120
121
  "activeThresholdMs": 8000, // "working" if Claude was active within this window
121
122
  "sleepThresholdMs": 300000, // "sleeping" after this much idle time (5 min)
package/config.json CHANGED
@@ -2,6 +2,7 @@
2
2
  "_note": "Defaults bundled with the app. User settings are saved to ~/.claude-usage-monitor/config.json. Session/weekly % come from your account (log in via Settings).",
3
3
  "alerts": true,
4
4
  "alertThresholds": [80, 95],
5
+ "fireThreshold": 90,
5
6
  "pollIntervalMs": 4000,
6
7
  "activeThresholdMs": 8000,
7
8
  "sleepThresholdMs": 300000
package/main.js CHANGED
@@ -38,6 +38,7 @@ function publicConfig(c) {
38
38
  weeklyAnchorIso: c.weeklyAnchorIso,
39
39
  alerts: c.alerts,
40
40
  alertThresholds: c.alertThresholds,
41
+ fireThreshold: c.fireThreshold,
41
42
  }
42
43
  }
43
44
 
@@ -49,6 +50,7 @@ function loadConfig() {
49
50
  weeklyAnchorIso: null,
50
51
  alerts: true,
51
52
  alertThresholds: [80, 95],
53
+ fireThreshold: 90, // session % at which the pet catches fire (tired still fixed at 100)
52
54
  pollIntervalMs: 4000,
53
55
  activeThresholdMs: 8000,
54
56
  sleepThresholdMs: 300000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clauddy",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "A cute desktop pet that tracks your Claude Code usage",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -31,6 +31,43 @@
31
31
 
32
32
  <!-- Claude pixel pet -->
33
33
  <div id="stage">
34
+ <!-- reusable gear shape -->
35
+ <svg width="0" height="0" aria-hidden="true" style="position: absolute">
36
+ <defs>
37
+ <g id="gear-unit">
38
+ <g id="gear-teeth">
39
+ <rect x="10.5" y="0.5" width="3" height="5" rx="0.5" />
40
+ <rect x="10.5" y="18.5" width="3" height="5" rx="0.5" />
41
+ <rect x="0.5" y="10.5" width="5" height="3" rx="0.5" />
42
+ <rect x="18.5" y="10.5" width="5" height="3" rx="0.5" />
43
+ </g>
44
+ <use href="#gear-teeth" transform="rotate(45 12 12)" />
45
+ <circle cx="12" cy="12" r="7.5" />
46
+ <circle cx="12" cy="12" r="2.6" fill="#0a0706" />
47
+ </g>
48
+ </defs>
49
+ </svg>
50
+ <!-- loose gears turning on the ground behind the pet (settings only) -->
51
+ <svg id="gears" viewBox="0 0 216 135" preserveAspectRatio="none">
52
+ <g transform="translate(150 118) scale(1.35)">
53
+ <g>
54
+ <use href="#gear-unit" x="-12" y="-12" />
55
+ <animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="3s" repeatCount="indefinite" />
56
+ </g>
57
+ </g>
58
+ <g transform="translate(188 124) scale(0.78)">
59
+ <g>
60
+ <use href="#gear-unit" x="-12" y="-12" />
61
+ <animateTransform attributeName="transform" type="rotate" from="360" to="0" dur="2.3s" repeatCount="indefinite" />
62
+ </g>
63
+ </g>
64
+ <g transform="translate(118 125) scale(0.6)">
65
+ <g>
66
+ <use href="#gear-unit" x="-12" y="-12" />
67
+ <animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="1.8s" repeatCount="indefinite" />
68
+ </g>
69
+ </g>
70
+ </svg>
34
71
  <svg id="scene" viewBox="0 0 216 135" preserveAspectRatio="none"></svg>
35
72
  <div id="shadow"></div>
36
73
  <div id="pet">
@@ -38,6 +75,21 @@
38
75
  <g id="body"></g>
39
76
  <g id="eyes"></g>
40
77
  <rect id="mouth" x="38" y="54" width="24" height="10" rx="2" />
78
+ <!-- mechanic mode (settings): hard hat + screwdriver -->
79
+ <g id="robot">
80
+ <!-- yellow hard hat: pixel-art bitmap, built in pet.js (buildHat) -->
81
+ <g id="hardhat"></g>
82
+ <!-- screwdriver held at an angle to the side, tapping away -->
83
+ <g id="driver">
84
+ <rect class="dr-tip" x="90" y="39" width="6" height="4" />
85
+ <rect class="dr-shaft" x="91" y="43" width="4" height="19" />
86
+ <rect class="dr-hi" x="91" y="43" width="1.6" height="19" />
87
+ <rect class="dr-collar" x="87" y="61" width="12" height="5" rx="1" />
88
+ <rect class="dr-handle" x="84" y="66" width="18" height="24" rx="8" />
89
+ <rect class="dr-handle-hi" x="86.5" y="69" width="2.6" height="17" rx="1.3" />
90
+ <animateTransform attributeName="transform" type="rotate" values="-32 93 70; -26 93 70; -32 93 70" dur="1s" repeatCount="indefinite" />
91
+ </g>
92
+ </g>
41
93
  </svg>
42
94
  <svg id="zzz" viewBox="0 0 44 44" width="44" height="44">
43
95
  <text x="2" y="34" class="z1">Z</text>
@@ -141,7 +193,7 @@
141
193
  </div>
142
194
 
143
195
  <label class="set-check"><input id="set-alerts" type="checkbox" /> Alerts</label>
144
- <div class="set-hint set-hint-left">macOS notifications when your session or weekly usage crosses these % thresholds.</div>
196
+ <div class="set-hint set-hint-left">A macOS notification when your session or weekly usage crosses each level.</div>
145
197
  <label class="set-field">Alert thresholds (%)
146
198
  <span class="set-two">
147
199
  <span class="num">
@@ -159,6 +211,23 @@
159
211
  </span>
160
212
  </span>
161
213
  </span>
214
+ <span class="set-two set-caps">
215
+ <span class="num-cap">heads-up</span>
216
+ <span class="num-cap">near limit</span>
217
+ </span>
218
+ </label>
219
+ <div class="set-hint set-hint-left">The pet catches fire past this session % (it's maxed out at 100%).</div>
220
+ <label class="set-field">Catch fire at (%)
221
+ <span class="set-two">
222
+ <span class="num">
223
+ <input id="set-fire" type="number" min="1" max="99" />
224
+ <span class="num-spin">
225
+ <button type="button" class="num-btn" data-for="set-fire" data-step="1">▲</button>
226
+ <button type="button" class="num-btn" data-for="set-fire" data-step="-1">▼</button>
227
+ </span>
228
+ </span>
229
+ <span class="num" aria-hidden="true"></span>
230
+ </span>
162
231
  </label>
163
232
  <div class="set-actions">
164
233
  <button id="set-cancel">Cancel</button>
package/renderer/pet.js CHANGED
@@ -40,6 +40,39 @@ const SPRITE = [
40
40
  })
41
41
  })()
42
42
 
43
+ // hard hat (mechanic mode): pixel-art bitmap so it matches the pet's blocky look
44
+ ;(function buildHat() {
45
+ const g = el('hardhat')
46
+ if (!g) return
47
+ // L = highlight, Y = main, D = shadow rim, B = brim
48
+ const HAT = [
49
+ '.....YLLY.....',
50
+ '...YYYLLYYY...',
51
+ '..YYYYLLYYYY..',
52
+ '.YYYYYLLYYYYY.',
53
+ '.DDDDDDDDDDDD.',
54
+ 'BBBBBBBBBBBBBB',
55
+ ]
56
+ const COL = { L: '#ffe27a', Y: '#f5c518', D: '#d99a00', B: '#c98f14' }
57
+ const C = 8 // cell size in svg units
58
+ const cols = HAT[0].length
59
+ const ox = 50 - (cols * C) / 2 // centered on the head (center x = 50)
60
+ const oy = -24 // rises above the head, brim ends just over the eyes
61
+ HAT.forEach((row, r) => {
62
+ for (let c = 0; c < row.length; c++) {
63
+ const ch = row[c]
64
+ if (ch === '.') continue
65
+ const rect = document.createElementNS(SVGNS, 'rect')
66
+ rect.setAttribute('x', ox + c * C)
67
+ rect.setAttribute('y', oy + r * C)
68
+ rect.setAttribute('width', C)
69
+ rect.setAttribute('height', C)
70
+ rect.setAttribute('fill', COL[ch])
71
+ g.appendChild(rect)
72
+ }
73
+ })
74
+ })()
75
+
43
76
  // night scene backdrop (clouds, crescent moon, stars, dotted ground/sky)
44
77
  ;(function buildScene() {
45
78
  const s = el('scene')
@@ -369,12 +402,13 @@ function render(d) {
369
402
  const wkPct = liveOn ? realUsage.week.pct : 0
370
403
  const wkReset = liveOn ? realUsage.week.resetMs : null
371
404
 
405
+ const fireAt = currentConfig.fireThreshold ?? 90
372
406
  let st =
373
407
  liveOn && sessPct >= 100
374
408
  ? 'tired'
375
409
  : d.active
376
410
  ? 'working'
377
- : liveOn && sessPct >= 90
411
+ : liveOn && sessPct >= fireAt
378
412
  ? 'stressed'
379
413
  : d.sleeping
380
414
  ? 'sleeping'
@@ -567,12 +601,32 @@ el('acc-confirm').addEventListener('click', () => {
567
601
  el('acc-logout').addEventListener('click', () => window.api.authLogout())
568
602
 
569
603
  // settings panel
604
+ // snapshot of the editable fields, to tell whether there are unsaved changes
605
+ let settingsBaseline = null
606
+ function snapshotSettings() {
607
+ return JSON.stringify({
608
+ alerts: el('set-alerts').checked,
609
+ t1: el('set-t1').value,
610
+ t2: el('set-t2').value,
611
+ fire: el('set-fire').value,
612
+ })
613
+ }
614
+ function refreshSaveDirty() {
615
+ if (settingsBaseline == null) return
616
+ el('set-save').classList.toggle('dirty', snapshotSettings() !== settingsBaseline)
617
+ }
618
+ function clearSaveDirty() {
619
+ settingsBaseline = snapshotSettings()
620
+ el('set-save').classList.remove('dirty')
621
+ }
570
622
  function populateSettings() {
571
623
  const c = currentConfig || {}
572
624
  el('set-alerts').checked = c.alerts !== false
573
625
  const th = c.alertThresholds || [80, 95]
574
626
  el('set-t1').value = th[0] != null ? th[0] : 80
575
627
  el('set-t2').value = th[1] != null ? th[1] : 95
628
+ el('set-fire').value = c.fireThreshold != null ? c.fireThreshold : 90
629
+ clearSaveDirty() // fields now match the saved config
576
630
  }
577
631
  function openSettings() {
578
632
  document.body.classList.remove('collapsed')
@@ -591,20 +645,30 @@ for (const b of document.querySelectorAll('.num-btn')) {
591
645
  const max = Number(input.max) || 100
592
646
  const next = (Number.parseInt(input.value, 10) || 0) + Number(b.dataset.step)
593
647
  input.value = Math.min(max, Math.max(min, next))
648
+ refreshSaveDirty() // steppers change the value without an 'input' event
594
649
  })
595
650
  }
651
+ // light up Save whenever an editable field changes
652
+ for (const id of ['set-alerts', 'set-t1', 'set-t2', 'set-fire']) {
653
+ el(id).addEventListener('input', refreshSaveDirty)
654
+ el(id).addEventListener('change', refreshSaveDirty)
655
+ }
596
656
  el('set-cancel').addEventListener('click', () => {
597
657
  document.body.classList.remove('settings-open')
658
+ clearSaveDirty()
598
659
  fitSize()
599
660
  })
600
661
  el('set-save').addEventListener('click', () => {
601
662
  const num = (id) => parseFloat(el(id).value)
663
+ const fire = num('set-fire')
602
664
  window.api.saveConfig({
603
665
  alerts: el('set-alerts').checked,
604
666
  alertThresholds: [num('set-t1'), num('set-t2')]
605
667
  .filter((n) => n >= 1 && n <= 100)
606
668
  .sort((a, b) => a - b),
669
+ fireThreshold: fire >= 1 && fire <= 99 ? fire : 90,
607
670
  })
671
+ clearSaveDirty()
608
672
  document.body.classList.remove('settings-open')
609
673
  fitSize()
610
674
  })
@@ -242,6 +242,16 @@ body.settings-open #settings {
242
242
  display: flex;
243
243
  gap: 6px;
244
244
  }
245
+ /* small captions under each threshold box */
246
+ .set-caps {
247
+ margin-top: 3px;
248
+ }
249
+ .num-cap {
250
+ flex: 1;
251
+ font-size: 8.5px;
252
+ color: var(--muted);
253
+ padding-left: 2px;
254
+ }
245
255
  /* custom number stepper */
246
256
  .num {
247
257
  position: relative;
@@ -324,12 +334,26 @@ body.settings-open #settings {
324
334
  background: rgba(255, 255, 255, 0.2);
325
335
  }
326
336
  #set-save {
327
- background: var(--coral);
337
+ background: rgba(217, 119, 87, 0.4); /* calm until there are unsaved changes */
328
338
  color: #fff;
329
339
  }
330
340
  #set-save:hover {
331
341
  background: #e2876a;
332
342
  }
343
+ /* unsaved changes: light up and pulse so you don't forget to save */
344
+ #set-save.dirty {
345
+ background: var(--coral);
346
+ animation: save-pulse 1.6s ease-in-out infinite;
347
+ }
348
+ @keyframes save-pulse {
349
+ 0%,
350
+ 100% {
351
+ box-shadow: 0 0 0 0 rgba(217, 119, 87, 0);
352
+ }
353
+ 50% {
354
+ box-shadow: 0 0 9px 2px rgba(217, 119, 87, 0.6);
355
+ }
356
+ }
333
357
 
334
358
  /* live tag */
335
359
  .live-only {
@@ -667,6 +691,51 @@ body.collapsed #shadow {
667
691
  transform-origin: center;
668
692
  animation: blink 5.5s infinite;
669
693
  }
694
+ /* mechanic mode (settings): hard hat + screwdriver on the pet, loose gears
695
+ turning behind it. Hidden otherwise. */
696
+ #robot {
697
+ opacity: 0;
698
+ pointer-events: none;
699
+ transition: opacity 0.2s ease;
700
+ }
701
+ /* hard hat colors live inline (built pixel-by-pixel in pet.js buildHat) */
702
+ .dr-handle {
703
+ fill: #c0392b;
704
+ }
705
+ .dr-handle-hi {
706
+ fill: #e0685a;
707
+ }
708
+ .dr-collar {
709
+ fill: #9a9a9a;
710
+ }
711
+ .dr-tip,
712
+ .dr-shaft {
713
+ fill: #d0d0d0;
714
+ }
715
+ .dr-hi {
716
+ fill: #f2f2f2;
717
+ }
718
+ #gears {
719
+ position: absolute;
720
+ inset: 0;
721
+ width: 100%;
722
+ height: 100%;
723
+ z-index: 1;
724
+ fill: #7a6a5c;
725
+ opacity: 0;
726
+ pointer-events: none;
727
+ transition: opacity 0.2s ease;
728
+ }
729
+ body.settings-open #robot,
730
+ body.settings-open #gears {
731
+ opacity: 1;
732
+ }
733
+ /* declutter the scene while configuring */
734
+ body.settings-open #zzz,
735
+ body.settings-open #flames,
736
+ body.settings-open #sweat {
737
+ display: none;
738
+ }
670
739
  #mouth {
671
740
  fill: var(--eye);
672
741
  transform-box: fill-box;