Skip to content

Commit 8c5d3e1

Browse files
chore: merge claudevisor-rename into main
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2 parents ab4bcc3 + 70d1f23 commit 8c5d3e1

9 files changed

Lines changed: 99 additions & 75 deletions

File tree

.github/workflows/build.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,26 +39,26 @@ jobs:
3939
- name: Create DMG
4040
run: |
4141
TMP_DIR=$(mktemp -d)
42-
cp -r dist/ClaudeWatch.app "$TMP_DIR/"
42+
cp -r dist/ClaudeMonitor.app "$TMP_DIR/"
4343
ln -s /Applications "$TMP_DIR/Applications"
4444
hdiutil create \
45-
-volname "ClaudeWatch" \
45+
-volname "ClaudeMonitor" \
4646
-srcfolder "$TMP_DIR" \
4747
-ov -format UDZO \
48-
"ClaudeWatch-${{ env.VERSION }}.dmg"
48+
"ClaudeMonitor-${{ env.VERSION }}.dmg"
4949
rm -rf "$TMP_DIR"
5050
5151
- name: Create GitHub Release
5252
uses: softprops/action-gh-release@v2
5353
with:
54-
files: ClaudeWatch-${{ env.VERSION }}.dmg
54+
files: ClaudeMonitor-${{ env.VERSION }}.dmg
5555
generate_release_notes: true
5656
body: |
5757
## Install
5858
59-
1. Download **ClaudeWatch-${{ env.VERSION }}.dmg**
60-
2. Open the DMG and drag **ClaudeWatch** to your Applications folder
59+
1. Download **ClaudeMonitor-${{ env.VERSION }}.dmg**
60+
2. Open the DMG and drag **ClaudeMonitor** to your Applications folder
6161
3. Right-click → **Open** on first launch (required — app is unsigned)
62-
4. ClaudeWatch will ask if you'd like it to start automatically at login
62+
4. ClaudeMonitor will ask if you'd like it to start automatically at login
6363
6464
**Requirements:** macOS · Claude desktop app installed and signed in

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@ venv/
1212
# macOS
1313
.DS_Store
1414

15+
# Brainstorming session files
16+
.superpowers/
17+
1518
# Logs
1619
*.log

HANDOFF.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ClaudeWatch — Claude Code Handoff
1+
# ClaudeMonitor — Claude Code Handoff
22

33
## What this is
44
A macOS menu bar app that shows Claude.ai usage percentages in real time, reading directly from the Claude desktop app's encrypted session cookies. No manual cookie setup required — this is the key differentiator vs. competitors.
@@ -88,7 +88,7 @@ Note: `seven_day_opus`, `seven_day_sonnet` etc. are null when not used but will
8888
"is_temporary": false,
8989
"platform": "CLAUDE_AI",
9090
"project_uuid": "...",
91-
"project": { "uuid": "...", "name": "ClaudeWatch" },
91+
"project": { "uuid": "...", "name": "ClaudeMonitor" },
9292
"settings": { "enabled_web_search": true, "enabled_mcp_tools": {...}, ... }
9393
}
9494
```
@@ -164,12 +164,12 @@ Trigger macOS notifications at 25%, 50%, 75%, 90% thresholds for both 5h and 7d
164164

165165
| Product | Auth method | Token counts | Notifications | Price |
166166
|---------|-------------|--------------|---------------|-------|
167-
| **ClaudeWatch** | Auto (desktop app cookies) ✅ | TBD | Not yet | TBD |
167+
| **ClaudeMonitor** | Auto (desktop app cookies) ✅ | TBD | Not yet | TBD |
168168
| ClaudeUsageBar | Manual cookie paste | No | Yes (25/50/75/90%) | Free |
169169
| Usagebar | Claude Code keychain | No | Yes (50/75/90%) | PWYW |
170170
| ClaudeTuner | Unknown | Yes | Yes | Freemium |
171171

172-
ClaudeWatch's moat: **zero setup**. Every competitor requires manual cookie copying. This is the headline feature for marketing.
172+
ClaudeMonitor's moat: **zero setup**. Every competitor requires manual cookie copying. This is the headline feature for marketing.
173173

174174
---
175175

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ClaudeWatch — Mac Menu Bar Usage Monitor
1+
# ClaudeMonitor — Mac Menu Bar Usage Monitor
22

33
A lightweight Mac menu bar app that shows your Claude.ai usage percentages in real time, using the Claude desktop app's session cookies for authentication.
44

@@ -27,9 +27,9 @@ Click the icon to see reset times, extra credit usage, and a link to the full us
2727

2828
## Installing the DMG
2929

30-
> **Gatekeeper warning:** ClaudeWatch is not signed with an Apple Developer certificate, so macOS will block it on first launch.
30+
> **Gatekeeper warning:** ClaudeMonitor is not signed with an Apple Developer certificate, so macOS will block it on first launch.
3131
>
32-
> To open it: **right-click** (or Control-click) `ClaudeWatch.app`**Open****Open** again in the dialog. You only need to do this once.
32+
> To open it: **right-click** (or Control-click) `ClaudeMonitor.app`**Open****Open** again in the dialog. You only need to do this once.
3333
3434
---
3535

@@ -85,10 +85,10 @@ Response:
8585

8686
```bash
8787
# Stop
88-
launchctl unload ~/Library/LaunchAgents/com.katespurr.claudewatch.plist
88+
launchctl unload ~/Library/LaunchAgents/com.katespurr.claudemonitor.plist
8989

9090
# Start
91-
launchctl load ~/Library/LaunchAgents/com.katespurr.claudewatch.plist
91+
launchctl load ~/Library/LaunchAgents/com.katespurr.claudemonitor.plist
9292

9393
# Logs
9494
tail -f ~/.claude_monitor/stderr.log

build_dmg.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/bin/bash
22
# ─────────────────────────────────────────────────────────────────────────────
3-
# ClaudeWatch — Build Script
3+
# ClaudeMonitor — Build Script
44
#
5-
# Builds ClaudeWatch.app via py2app and packages it as a DMG.
5+
# Builds ClaudeMonitor.app via py2app and packages it as a DMG.
66
#
77
# Usage:
88
# ./build_dmg.sh Build app + DMG
@@ -13,7 +13,7 @@ set -euo pipefail
1313

1414
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
1515
BUILD_VENV="$SCRIPT_DIR/.build_venv"
16-
APP_NAME="ClaudeWatch"
16+
APP_NAME="ClaudeMonitor"
1717
DMG_NAME="$APP_NAME.dmg"
1818
APP_ONLY=false
1919

@@ -29,7 +29,7 @@ bold() { printf '\033[1m%s\033[0m\n' "$*"; }
2929
# ── Pre-flight ────────────────────────────────────────────────────────────────
3030

3131
echo ""
32-
bold "ClaudeWatch — Build"
32+
bold "ClaudeMonitor — Build"
3333
echo "─────────────────────────────────────"
3434
echo ""
3535

claude_monitor.py

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -886,11 +886,13 @@ def _conversation_insights_html(conversations):
886886
return ""
887887

888888
# Estimate relative cost from model and recency (more recent = more active = higher cost)
889+
import re as _re
889890
ranked = []
890891
now = datetime.now(timezone.utc)
891892
for c in conversations[:20]:
892893
model_id, model_label = _get_conversation_model(c)
893-
weight = MODEL_COST_WEIGHT.get(model_id, 1.0)
894+
base_model_id = _re.sub(r"-\d{8}$", "", model_id)
895+
weight = MODEL_COST_WEIGHT.get(model_id, MODEL_COST_WEIGHT.get(base_model_id, 1.0))
894896
name = c.get("name") or "Untitled"
895897
uuid = c.get("uuid", "")
896898
updated = c.get("updated_at", "")
@@ -1334,7 +1336,7 @@ def generate_dashboard(usage, stats, conversations=None):
13341336
<head>
13351337
<meta charset="UTF-8">
13361338
<meta name="viewport" content="width=device-width,initial-scale=1">
1337-
<title>ClaudeWatch Dashboard</title>
1339+
<title>ClaudeMonitor Dashboard</title>
13381340
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
13391341
<style>
13401342
*{{box-sizing:border-box;margin:0;padding:0}}
@@ -1396,7 +1398,7 @@ def generate_dashboard(usage, stats, conversations=None):
13961398
<circle cx="50" cy="50" r="48" fill="#0D9488"/>
13971399
<text x="50" y="67" text-anchor="middle" font-size="52" fill="white" font-family="-apple-system">◈</text>
13981400
</svg>
1399-
ClaudeWatch
1401+
ClaudeMonitor
14001402
</h1>
14011403
<p class="sub">Updated {now_str}</p>
14021404
@@ -1447,7 +1449,7 @@ def generate_dashboard(usage, stats, conversations=None):
14471449
{conversations_html}
14481450
{_model_guide_html(usage, stats)}
14491451
1450-
<div class="footer">ClaudeWatch · data from Claude desktop app · <a href="{CLAUDE_USAGE_URL}" style="color:var(--accent)">open claude.ai</a></div>
1452+
<div class="footer">ClaudeMonitor · data from Claude desktop app · <a href="{CLAUDE_USAGE_URL}" style="color:var(--accent)">open claude.ai</a></div>
14511453
</body>
14521454
</html>"""
14531455

@@ -1535,7 +1537,7 @@ def _build_conversations_html(conversations):
15351537
</tr>\n"""
15361538

15371539
return f"""<!DOCTYPE html>
1538-
<html><head><meta charset="utf-8"><title>ClaudeWatch — Recent Conversations</title>
1540+
<html><head><meta charset="utf-8"><title>ClaudeMonitor — Recent Conversations</title>
15391541
<style>
15401542
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
15411543
margin: 2rem; background: #1a1a2e; color: #e0e0e0; }}
@@ -1616,15 +1618,33 @@ def _build_title(usage, config, velocity=None, recent_session_pcts=None):
16161618
return title
16171619

16181620

1621+
def _apply_colored_title(app, title, session_pct, weekly_pct):
1622+
"""Set the menu bar title with color using NSAttributedString."""
1623+
try:
1624+
import AppKit
1625+
pct = max(session_pct or 0, weekly_pct or 0)
1626+
if pct >= 80:
1627+
color = AppKit.NSColor.systemRedColor()
1628+
elif pct >= 60:
1629+
color = AppKit.NSColor.systemOrangeColor()
1630+
else:
1631+
color = AppKit.NSColor.labelColor()
1632+
attrs = {AppKit.NSForegroundColorAttributeName: color}
1633+
attr_str = AppKit.NSAttributedString.alloc().initWithString_attributes_(title, attrs)
1634+
app._nsapp.nsstatusitem.setAttributedTitle_(attr_str)
1635+
except Exception:
1636+
pass # plain title already set as fallback
1637+
1638+
16191639
def _build_tooltip(usage, velocity=None, runway=None, msg_est=None):
16201640
if usage is None:
1621-
return "ClaudeWatch — no data"
1641+
return "ClaudeMonitor — no data"
16221642
spct = usage.get("session_pct", 0)
16231643
wpct = usage.get("weekly_pct", 0)
16241644
sr = _fmt_reset(usage.get("session_resets_at", ""))
16251645
wr = _fmt_reset(usage.get("weekly_resets_at", ""))
16261646
lines = [
1627-
"ClaudeWatch",
1647+
"ClaudeMonitor",
16281648
f"Session (5h): {spct:.0f}%" + (f" — resets in {sr}" if sr else ""),
16291649
f"Weekly (7d): {wpct:.0f}%" + (f" — resets in {wr}" if wr else ""),
16301650
]
@@ -1798,30 +1818,30 @@ def _info(label):
17981818

17991819
# First-run: offer login item when running as bundled .app
18001820
if getattr(_sys, 'frozen', False):
1801-
_plist = Path.home() / "Library/LaunchAgents/com.katespurr.claudewatch.plist"
1821+
_plist = Path.home() / "Library/LaunchAgents/com.katespurr.claudemonitor.plist"
18021822
if not _plist.exists():
18031823
self._first_run_timer = rumps.Timer(self._prompt_login_item, 1.5)
18041824
self._first_run_timer.start()
18051825

18061826
def _prompt_login_item(self, timer):
18071827
timer.stop()
18081828
response = rumps.alert(
1809-
title="Welcome to ClaudeWatch",
1810-
message="Start ClaudeWatch automatically when you log in?",
1829+
title="Welcome to ClaudeMonitor",
1830+
message="Start ClaudeMonitor automatically when you log in?",
18111831
ok="Yes, start at login",
18121832
cancel="Not now",
18131833
)
18141834
if response == 1:
1815-
executable = Path(_sys.executable).parent / "ClaudeWatch"
1816-
plist_file = Path.home() / "Library/LaunchAgents/com.katespurr.claudewatch.plist"
1835+
executable = Path(_sys.executable).parent / "ClaudeMonitor"
1836+
plist_file = Path.home() / "Library/LaunchAgents/com.katespurr.claudemonitor.plist"
18171837
plist_file.parent.mkdir(parents=True, exist_ok=True)
18181838
plist_file.write_text(f"""<?xml version="1.0" encoding="UTF-8"?>
18191839
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
18201840
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
18211841
<plist version="1.0">
18221842
<dict>
18231843
<key>Label</key>
1824-
<string>com.katespurr.claudewatch</string>
1844+
<string>com.katespurr.claudemonitor</string>
18251845
<key>ProgramArguments</key>
18261846
<array>
18271847
<string>{executable}</string>
@@ -1837,8 +1857,8 @@ def _prompt_login_item(self, timer):
18371857
</dict>
18381858
</plist>""")
18391859
subprocess.run(["launchctl", "load", str(plist_file)], check=False)
1840-
rumps.notification("ClaudeWatch", "Login item installed",
1841-
"ClaudeWatch will start automatically at login.")
1860+
rumps.notification("ClaudeMonitor", "Login item installed",
1861+
"ClaudeMonitor will start automatically at login.")
18421862

18431863
# ── Settings ──
18441864

@@ -1909,7 +1929,7 @@ def open_dashboard(self, _):
19091929
path = generate_dashboard(self._usage, self._stats, getattr(self, "_conversations", None))
19101930
webbrowser.open(f"file://{path}")
19111931
else:
1912-
rumps.notification("ClaudeWatch", "No data yet", "Fetching usage now…")
1932+
rumps.notification("ClaudeMonitor", "No data yet", "Fetching usage now…")
19131933
threading.Thread(target=self._refresh, daemon=True).start()
19141934

19151935
@rumps.clicked("Recent Conversations")
@@ -1925,7 +1945,7 @@ def _fetch_conversations(self, sender):
19251945
path.write_text(html)
19261946
webbrowser.open(f"file://{path}")
19271947
except Exception as e:
1928-
rumps.notification("ClaudeWatch", "Error loading conversations", str(e)[:100])
1948+
rumps.notification("ClaudeMonitor", "Error loading conversations", str(e)[:100])
19291949
finally:
19301950
sender.title = "Recent Conversations"
19311951

@@ -1938,7 +1958,7 @@ def quit_app(self, _):
19381958
rumps.quit_application()
19391959

19401960
def restart_app(self, _):
1941-
rumps.notification("ClaudeWatch", "Restarting…", "")
1961+
rumps.notification("ClaudeMonitor", "Restarting…", "")
19421962
rumps.quit_application()
19431963

19441964
# ── Refresh interval ──
@@ -2169,7 +2189,7 @@ def _check_limits(self, usage):
21692189
# Notify on new session (but not on first launch)
21702190
if old_reset is not None and sreset:
21712191
rumps.notification(
2172-
"ClaudeWatch",
2192+
"ClaudeMonitor",
21732193
"New session started",
21742194
"Fresh 5-hour window — slate is clean.",
21752195
)
@@ -2181,14 +2201,14 @@ def _check_limits(self, usage):
21812201
key = f"session_{t}"
21822202
if spct >= t and key not in self._notified:
21832203
self._notified.add(key)
2184-
rumps.notification("ClaudeWatch", f"Session usage at {t}%",
2204+
rumps.notification("ClaudeMonitor", f"Session usage at {t}%",
21852205
f"Resets in {sr}." if sr else "")
21862206

21872207
for t in WEEKLY_THRESHOLDS:
21882208
key = f"weekly_{t}"
21892209
if wpct >= t and key not in self._notified:
21902210
self._notified.add(key)
2191-
rumps.notification("ClaudeWatch", f"Weekly usage at {t}%",
2211+
rumps.notification("ClaudeMonitor", f"Weekly usage at {t}%",
21922212
f"Resets in {wr}." if wr else "")
21932213

21942214
extra_pct = usage.get("extra_pct")
@@ -2198,7 +2218,7 @@ def _check_limits(self, usage):
21982218
key = f"extra_{t}"
21992219
if extra_pct >= t and key not in self._notified:
22002220
self._notified.add(key)
2201-
rumps.notification("ClaudeWatch", f"Extra credits at {t}%",
2221+
rumps.notification("ClaudeMonitor", f"Extra credits at {t}%",
22022222
f"{eu:.0f} credits used")
22032223

22042224
def _apply_ui(self, usage, error=None):
@@ -2210,7 +2230,7 @@ def _apply_ui(self, usage, error=None):
22102230
if error:
22112231
self.title = "!"
22122232
self.menu[session_detail].title = f" {error[:60]}"
2213-
self._set_tooltip(f"ClaudeWatch — error\n{error[:120]}")
2233+
self._set_tooltip(f"ClaudeMonitor — error\n{error[:120]}")
22142234
return
22152235

22162236
velocity = getattr(self, "_velocity", None)
@@ -2220,6 +2240,7 @@ def _apply_ui(self, usage, error=None):
22202240
recent_pcts = getattr(self, "_recent_session_pcts", None)
22212241

22222242
self.title = _build_title(usage, self.config, velocity, recent_pcts)
2243+
_apply_colored_title(self, self.title, usage.get("session_pct", 0), usage.get("weekly_pct", 0))
22232244

22242245
if self.config.get("show_hover_tooltip", True):
22252246
self._set_tooltip(_build_tooltip(usage, velocity, runway, msg_est))

setup.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"""
2-
py2app build script for ClaudeWatch.
2+
py2app build script for ClaudeMonitor.
33
44
Usage:
55
pip install py2app
66
python setup.py py2app
77
8-
Output: dist/ClaudeWatch.app
8+
Output: dist/ClaudeMonitor.app
99
"""
1010

1111
from setuptools import setup
@@ -21,9 +21,9 @@
2121
"argv_emulation": False,
2222
"iconfile": "ClaudeWatch.icns",
2323
"plist": {
24-
"CFBundleName": "ClaudeWatch",
25-
"CFBundleDisplayName": "ClaudeWatch",
26-
"CFBundleIdentifier": "com.katespurr.claudewatch",
24+
"CFBundleName": "ClaudeMonitor",
25+
"CFBundleDisplayName": "ClaudeMonitor",
26+
"CFBundleIdentifier": "com.katespurr.claudemonitor",
2727
"CFBundleVersion": "1.0.0",
2828
"CFBundleShortVersionString": "1.0.0",
2929
"LSUIElement": True, # menu bar only — no Dock icon
@@ -34,7 +34,7 @@
3434
}
3535

3636
setup(
37-
name="ClaudeWatch",
37+
name="ClaudeMonitor",
3838
app=APP,
3939
data_files=DATA_FILES,
4040
options={"py2app": OPTIONS},

0 commit comments

Comments
 (0)