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.
- package/.claude/commands/feed.md +28 -0
- package/.claude/commands/name.md +28 -0
- package/.claude/commands/pet.md +29 -0
- package/.claude/commands/play.md +29 -0
- package/.claude/settings.local.json +41 -0
- package/.github/workflows/AGENTS.md +60 -0
- package/.github/workflows/build.yml +87 -0
- package/AGENTS.md +66 -0
- package/LICENSE +15 -0
- package/README.md +292 -0
- package/bin/claude-pet.js +42 -0
- package/build/AGENTS.md +50 -0
- package/build/dmg-background.png +0 -0
- package/build/entitlements.mac.plist +14 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/installerHeader.bmp +0 -0
- package/build/installerSidebar.bmp +0 -0
- package/build/tray-icon.png +0 -0
- package/dist/main/core/badge-manager.js +49 -0
- package/dist/main/core/badge-registry.js +72 -0
- package/dist/main/core/badge-triggers.js +45 -0
- package/dist/main/core/contextual-messages.js +372 -0
- package/dist/main/core/messages.js +440 -0
- package/dist/main/core/mood-engine.js +145 -0
- package/dist/main/core/pet-messages.js +612 -0
- package/dist/main/core/pet-state-engine.js +232 -0
- package/dist/main/core/quote-collection.js +60 -0
- package/dist/main/core/quote-registry.js +175 -0
- package/dist/main/core/quote-triggers.js +62 -0
- package/dist/main/core/usage-tracker.js +625 -0
- package/dist/main/main/auto-launch.js +39 -0
- package/dist/main/main/auto-updater.js +98 -0
- package/dist/main/main/event-watcher.js +174 -0
- package/dist/main/main/ipc-handlers.js +89 -0
- package/dist/main/main/main.js +422 -0
- package/dist/main/main/preload.js +93 -0
- package/dist/main/main/settings-window.js +49 -0
- package/dist/main/main/share-card.js +139 -0
- package/dist/main/main/skin-manager.js +118 -0
- package/dist/main/main/tray.js +88 -0
- package/dist/main/shared/i18n.js +392 -0
- package/dist/main/shared/types.js +25 -0
- package/dist/main/shared/utils.js +9 -0
- package/dist/renderer/assets/claude-pet.png +0 -0
- package/dist/renderer/assets/index-BMnMEuOf.js +9 -0
- package/dist/renderer/assets/index-qzlrlqpX.css +1 -0
- package/dist/renderer/index.html +30 -0
- package/dist/renderer/share-card-template/card.html +148 -0
- package/docs/AGENTS.md +42 -0
- package/docs/images/angry.png +0 -0
- package/docs/images/character.webp +0 -0
- package/docs/images/claude-mama.png +0 -0
- package/docs/images/happy.png +0 -0
- package/docs/images/proud.png +0 -0
- package/docs/images/share-card-example.png +0 -0
- package/docs/images/worried_1.png +0 -0
- package/docs/images/worried_2.png +0 -0
- package/docs/spritesheet-bugs.md +240 -0
- package/docs/superpowers/plans/2026-03-10-compact-widget.md +888 -0
- package/docs/superpowers/plans/2026-03-10-viral-features.md +1874 -0
- package/docs/superpowers/plans/2026-03-14-update-ux.md +362 -0
- package/docs/superpowers/plans/2026-03-14-v1.1-features.md +2139 -0
- package/docs/superpowers/specs/2026-03-10-compact-widget-design.md +150 -0
- package/docs/superpowers/specs/2026-03-10-viral-features-design.md +217 -0
- package/docs/superpowers/specs/2026-03-14-streak-calendar-design.md +26 -0
- package/docs/superpowers/specs/2026-03-14-update-ux-design.md +172 -0
- package/docs/superpowers/specs/2026-03-14-v1.1-features-design.md +342 -0
- package/electron-builder.yml +75 -0
- package/package.json +48 -0
- package/scripts/AGENTS.md +60 -0
- package/scripts/install.ps1 +47 -0
- package/scripts/install.sh +98 -0
- package/scripts/make-icon.js +119 -0
- package/scripts/notarize.js +18 -0
- package/src/AGENTS.md +47 -0
- package/src/core/AGENTS.md +58 -0
- package/src/core/__tests__/AGENTS.md +60 -0
- package/src/core/__tests__/badge-triggers.test.ts +83 -0
- package/src/core/__tests__/contextual-messages.test.ts +87 -0
- package/src/core/__tests__/pet-state-engine.test.ts +350 -0
- package/src/core/__tests__/quote-collection.test.ts +62 -0
- package/src/core/__tests__/quote-triggers.test.ts +110 -0
- package/src/core/badge-manager.ts +50 -0
- package/src/core/badge-registry.ts +71 -0
- package/src/core/badge-triggers.ts +41 -0
- package/src/core/contextual-messages.ts +381 -0
- package/src/core/pet-messages.ts +615 -0
- package/src/core/pet-state-engine.ts +272 -0
- package/src/core/quote-collection.ts +63 -0
- package/src/core/quote-registry.ts +181 -0
- package/src/core/quote-triggers.ts +64 -0
- package/src/core/usage-tracker.ts +680 -0
- package/src/main/AGENTS.md +70 -0
- package/src/main/auto-launch.ts +38 -0
- package/src/main/auto-updater.ts +106 -0
- package/src/main/event-watcher.ts +159 -0
- package/src/main/ipc-handlers.ts +107 -0
- package/src/main/main.ts +425 -0
- package/src/main/preload.ts +111 -0
- package/src/main/settings-window.ts +50 -0
- package/src/main/share-card.ts +153 -0
- package/src/main/skin-manager.ts +119 -0
- package/src/main/tray.ts +94 -0
- package/src/renderer/AGENTS.md +62 -0
- package/src/renderer/App.tsx +270 -0
- package/src/renderer/assets/claude-mama.png +0 -0
- package/src/renderer/assets/claude-pet.png +0 -0
- package/src/renderer/components/AGENTS.md +50 -0
- package/src/renderer/components/Character.tsx +327 -0
- package/src/renderer/components/SpeechBubble.tsx +182 -0
- package/src/renderer/components/UsageIndicator.tsx +268 -0
- package/src/renderer/electron.d.ts +34 -0
- package/src/renderer/hooks/AGENTS.md +55 -0
- package/src/renderer/hooks/usePetState.ts +59 -0
- package/src/renderer/hooks/useWidgetMode.ts +18 -0
- package/src/renderer/index.html +29 -0
- package/src/renderer/main.tsx +13 -0
- package/src/renderer/pages/AGENTS.md +53 -0
- package/src/renderer/pages/Collection.tsx +252 -0
- package/src/renderer/pages/Settings.tsx +815 -0
- package/src/renderer/share-card-template/card.html +148 -0
- package/src/renderer/styles/AGENTS.md +50 -0
- package/src/renderer/styles/styles.css +166 -0
- package/src/shared/AGENTS.md +48 -0
- package/src/shared/i18n.ts +395 -0
- package/src/shared/types.ts +163 -0
- package/src/shared/utils.ts +6 -0
- package/tsconfig.json +16 -0
- package/tsconfig.main.json +12 -0
- package/tsconfig.renderer.json +12 -0
- package/vite.config.ts +47 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* UsageTracker — fetches Claude utilization data from the Anthropic OAuth usage API.
|
|
4
|
+
*
|
|
5
|
+
* API VERIFICATION NOTE (Task 0):
|
|
6
|
+
* The endpoint GET https://api.anthropic.com/api/oauth/usage returned:
|
|
7
|
+
* {"type":"error","error":{"type":"authentication_error",
|
|
8
|
+
* "message":"OAuth authentication is currently not supported."}}
|
|
9
|
+
* The token was valid but the endpoint does not yet support OAuth bearer tokens.
|
|
10
|
+
* Implementation uses the ASSUMED interface below; verify when the API becomes available:
|
|
11
|
+
*
|
|
12
|
+
* interface UsageApiResponse {
|
|
13
|
+
* five_hour?: { utilization?: number; resets_at?: string };
|
|
14
|
+
* seven_day?: { utilization?: number; resets_at?: string };
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* No Electron imports. Uses only Node built-ins: https, fs, os, path.
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.UsageTracker = void 0;
|
|
54
|
+
const https = __importStar(require("https"));
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
56
|
+
const os = __importStar(require("os"));
|
|
57
|
+
const path = __importStar(require("path"));
|
|
58
|
+
const child_process_1 = require("child_process");
|
|
59
|
+
const CREDENTIALS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
60
|
+
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
61
|
+
const CALIBRATION_PATH = path.join(os.homedir(), '.claude', 'mama-calibration.json');
|
|
62
|
+
const CALIBRATION_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
63
|
+
function loadCalibration() {
|
|
64
|
+
try {
|
|
65
|
+
const raw = fs.readFileSync(CALIBRATION_PATH, 'utf8');
|
|
66
|
+
return JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { fiveHour: null, sevenDay: null };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function saveCalibration(data) {
|
|
73
|
+
try {
|
|
74
|
+
fs.writeFileSync(CALIBRATION_PATH, JSON.stringify(data, null, 2), 'utf8');
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.log('[usage-tracker] Failed to save calibration:', err instanceof Error ? err.message : err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
81
|
+
/**
|
|
82
|
+
* Called after a successful API response to learn the relationship between
|
|
83
|
+
* JSONL token counts and API utilization percentages.
|
|
84
|
+
*/
|
|
85
|
+
function updateCalibration(apiData) {
|
|
86
|
+
if (apiData.dataSource !== 'api')
|
|
87
|
+
return;
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const fiveHoursAgo = now - 5 * 60 * 60 * 1000;
|
|
90
|
+
const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
|
|
91
|
+
const files = collectJsonlFiles();
|
|
92
|
+
if (files.length === 0)
|
|
93
|
+
return;
|
|
94
|
+
const fiveHourEntries = [];
|
|
95
|
+
const sevenDayEntries = [];
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
try {
|
|
98
|
+
const stat = fs.statSync(file);
|
|
99
|
+
if (stat.mtimeMs < sevenDaysAgo)
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const entries = parseJsonlTokenUsage(file, sevenDaysAgo);
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const entryTime = new Date(entry.timestamp).getTime();
|
|
108
|
+
sevenDayEntries.push(entry);
|
|
109
|
+
if (entryTime >= fiveHoursAgo) {
|
|
110
|
+
fiveHourEntries.push(entry);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const cal = loadCalibration();
|
|
115
|
+
const nowIso = new Date().toISOString();
|
|
116
|
+
// Only calibrate if API returned meaningful utilization (> 1%) and we have tokens
|
|
117
|
+
if (apiData.fiveHourUtilization && apiData.fiveHourUtilization > 1) {
|
|
118
|
+
const tokens = aggregateTokens(fiveHourEntries).total;
|
|
119
|
+
if (tokens > 0) {
|
|
120
|
+
cal.fiveHour = { tokensPerPercent: tokens / apiData.fiveHourUtilization, updatedAt: nowIso };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (apiData.weeklyUtilization && apiData.weeklyUtilization > 1) {
|
|
124
|
+
const tokens = aggregateTokens(sevenDayEntries).total;
|
|
125
|
+
if (tokens > 0) {
|
|
126
|
+
cal.sevenDay = { tokensPerPercent: tokens / apiData.weeklyUtilization, updatedAt: nowIso };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
saveCalibration(cal);
|
|
130
|
+
console.log(`[usage-tracker] Calibration updated: 5h=${cal.fiveHour?.tokensPerPercent?.toFixed(0) ?? 'n/a'} tok/%, 7d=${cal.sevenDay?.tokensPerPercent?.toFixed(0) ?? 'n/a'} tok/%`);
|
|
131
|
+
}
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// JSONL Session Parser — extract token usage from ~/.claude/projects/*/*.jsonl
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
function collectJsonlFiles() {
|
|
136
|
+
try {
|
|
137
|
+
if (!fs.existsSync(CLAUDE_PROJECTS_DIR))
|
|
138
|
+
return [];
|
|
139
|
+
const projectDirs = fs.readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true })
|
|
140
|
+
.filter(d => d.isDirectory())
|
|
141
|
+
.map(d => path.join(CLAUDE_PROJECTS_DIR, d.name));
|
|
142
|
+
const files = [];
|
|
143
|
+
for (const dir of projectDirs) {
|
|
144
|
+
try {
|
|
145
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
148
|
+
files.push(path.join(dir, entry.name));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch { /* skip unreadable dirs */ }
|
|
153
|
+
}
|
|
154
|
+
return files;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function parseJsonlTokenUsage(filePath, sinceMs) {
|
|
161
|
+
const results = [];
|
|
162
|
+
try {
|
|
163
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
164
|
+
const lines = content.split('\n');
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
if (!line.trim())
|
|
167
|
+
continue;
|
|
168
|
+
try {
|
|
169
|
+
const entry = JSON.parse(line);
|
|
170
|
+
if (entry.type !== 'assistant' || !entry.message?.usage)
|
|
171
|
+
continue;
|
|
172
|
+
const ts = entry.timestamp ?? entry.message?.timestamp;
|
|
173
|
+
if (!ts)
|
|
174
|
+
continue;
|
|
175
|
+
const entryTime = new Date(ts).getTime();
|
|
176
|
+
if (isNaN(entryTime) || entryTime < sinceMs)
|
|
177
|
+
continue;
|
|
178
|
+
const usage = entry.message.usage;
|
|
179
|
+
results.push({
|
|
180
|
+
input_tokens: usage.input_tokens ?? 0,
|
|
181
|
+
cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,
|
|
182
|
+
cache_read_input_tokens: usage.cache_read_input_tokens ?? 0,
|
|
183
|
+
output_tokens: usage.output_tokens ?? 0,
|
|
184
|
+
timestamp: ts,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
catch { /* skip malformed lines */ }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch { /* skip unreadable files */ }
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
193
|
+
function aggregateTokens(entries) {
|
|
194
|
+
let input = 0, cacheCreation = 0, cacheRead = 0, output = 0;
|
|
195
|
+
for (const e of entries) {
|
|
196
|
+
input += e.input_tokens;
|
|
197
|
+
cacheCreation += e.cache_creation_input_tokens;
|
|
198
|
+
cacheRead += e.cache_read_input_tokens;
|
|
199
|
+
output += e.output_tokens;
|
|
200
|
+
}
|
|
201
|
+
// Rate limit utilization is based on compute cost, not raw token count.
|
|
202
|
+
// cache_read tokens are ~10x cheaper, cache_creation ~25% cheaper than input.
|
|
203
|
+
// Use weighted total: input + output fully, cache_creation at 0.25, cache_read at 0.1
|
|
204
|
+
const weightedTotal = input + output
|
|
205
|
+
+ Math.round(cacheCreation * 0.25)
|
|
206
|
+
+ Math.round(cacheRead * 0.1);
|
|
207
|
+
return {
|
|
208
|
+
input, cacheCreation, cacheRead, output,
|
|
209
|
+
total: weightedTotal,
|
|
210
|
+
entryCount: entries.length,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function estimateUsageFromJsonl() {
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
const fiveHoursAgo = now - 5 * 60 * 60 * 1000;
|
|
216
|
+
const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
|
|
217
|
+
const files = collectJsonlFiles();
|
|
218
|
+
if (files.length === 0) {
|
|
219
|
+
console.log('[usage-tracker] JSONL fallback: no session files found');
|
|
220
|
+
return noData('Rate limited', true);
|
|
221
|
+
}
|
|
222
|
+
const fiveHourEntries = [];
|
|
223
|
+
const sevenDayEntries = [];
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
// Quick filter: skip files older than 7 days by mtime
|
|
226
|
+
try {
|
|
227
|
+
const stat = fs.statSync(file);
|
|
228
|
+
if (stat.mtimeMs < sevenDaysAgo)
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const entries = parseJsonlTokenUsage(file, sevenDaysAgo);
|
|
235
|
+
for (const entry of entries) {
|
|
236
|
+
const entryTime = new Date(entry.timestamp).getTime();
|
|
237
|
+
sevenDayEntries.push(entry);
|
|
238
|
+
if (entryTime >= fiveHoursAgo) {
|
|
239
|
+
fiveHourEntries.push(entry);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (sevenDayEntries.length === 0) {
|
|
244
|
+
console.log('[usage-tracker] JSONL fallback: no entries in 7-day window');
|
|
245
|
+
return noData('Rate limited', true);
|
|
246
|
+
}
|
|
247
|
+
const fiveHourTotals = aggregateTokens(fiveHourEntries);
|
|
248
|
+
const sevenDayTotals = aggregateTokens(sevenDayEntries);
|
|
249
|
+
const cal = loadCalibration();
|
|
250
|
+
const calAge5h = cal.fiveHour ? Date.now() - new Date(cal.fiveHour.updatedAt).getTime() : Infinity;
|
|
251
|
+
const calAge7d = cal.sevenDay ? Date.now() - new Date(cal.sevenDay.updatedAt).getTime() : Infinity;
|
|
252
|
+
// Use calibration if available and fresh (< 24h), otherwise fall back to cache
|
|
253
|
+
const has5hCal = cal.fiveHour && calAge5h < CALIBRATION_MAX_AGE_MS;
|
|
254
|
+
const has7dCal = cal.sevenDay && calAge7d < CALIBRATION_MAX_AGE_MS;
|
|
255
|
+
if (!has5hCal && !has7dCal) {
|
|
256
|
+
console.log('[usage-tracker] JSONL fallback: no fresh calibration data');
|
|
257
|
+
return noData('Rate limited', true);
|
|
258
|
+
}
|
|
259
|
+
const fiveHourUtil = has5hCal
|
|
260
|
+
? Math.min(100, fiveHourTotals.total / cal.fiveHour.tokensPerPercent)
|
|
261
|
+
: null;
|
|
262
|
+
const weeklyUtil = has7dCal
|
|
263
|
+
? Math.min(100, sevenDayTotals.total / cal.sevenDay.tokensPerPercent)
|
|
264
|
+
: null;
|
|
265
|
+
console.log(`[usage-tracker] JSONL fallback: 5h=${fiveHourTotals.total} tokens (${fiveHourUtil?.toFixed(1) ?? 'n/a'}%), 7d=${sevenDayTotals.total} tokens (${weeklyUtil?.toFixed(1) ?? 'n/a'}%)`);
|
|
266
|
+
return {
|
|
267
|
+
weeklyUtilization: weeklyUtil,
|
|
268
|
+
fiveHourUtilization: fiveHourUtil,
|
|
269
|
+
resetsAt: null,
|
|
270
|
+
fiveHourResetsAt: null,
|
|
271
|
+
dataSource: 'jsonl',
|
|
272
|
+
stale: false,
|
|
273
|
+
rateLimited: true,
|
|
274
|
+
error: null,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// Credential reading
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
function readAccessTokenFromFile() {
|
|
281
|
+
try {
|
|
282
|
+
const raw = fs.readFileSync(CREDENTIALS_PATH, 'utf8');
|
|
283
|
+
const parsed = JSON.parse(raw);
|
|
284
|
+
return parsed?.claudeAiOauth?.accessToken ?? null;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// macOS Keychain — use absolute path to avoid PATH issues in packaged apps
|
|
291
|
+
const SECURITY_BIN = '/usr/bin/security';
|
|
292
|
+
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
293
|
+
let keychainCache = null;
|
|
294
|
+
const KEYCHAIN_CACHE_TTL_MS = 4 * 60 * 1000; // cache for 4 min (poll is every 5 min)
|
|
295
|
+
function parseKeychainToken(raw) {
|
|
296
|
+
try {
|
|
297
|
+
const parsed = JSON.parse(raw);
|
|
298
|
+
return parsed?.claudeAiOauth?.accessToken ?? null;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function readAccessTokenFromKeychain() {
|
|
305
|
+
// Return cached value if fresh enough
|
|
306
|
+
if (keychainCache && Date.now() - keychainCache.readAt < KEYCHAIN_CACHE_TTL_MS) {
|
|
307
|
+
return keychainCache.token;
|
|
308
|
+
}
|
|
309
|
+
// Strategy 1: Direct security command
|
|
310
|
+
const token = readKeychainDirect();
|
|
311
|
+
if (token) {
|
|
312
|
+
keychainCache = { token, readAt: Date.now() };
|
|
313
|
+
return token;
|
|
314
|
+
}
|
|
315
|
+
// Strategy 2: osascript wrapper — runs the security command via AppleScript,
|
|
316
|
+
// which executes in a user-interactive context and can show the macOS Keychain
|
|
317
|
+
// access dialog that packaged Electron apps often fail to trigger.
|
|
318
|
+
const tokenViaOsascript = readKeychainViaOsascript();
|
|
319
|
+
if (tokenViaOsascript) {
|
|
320
|
+
keychainCache = { token: tokenViaOsascript, readAt: Date.now() };
|
|
321
|
+
// Persist to credentials file so future reads don't need keychain
|
|
322
|
+
persistCredentialsFile(tokenViaOsascript);
|
|
323
|
+
return tokenViaOsascript;
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
function readKeychainDirect() {
|
|
328
|
+
try {
|
|
329
|
+
if (!fs.existsSync(SECURITY_BIN)) {
|
|
330
|
+
console.log('[usage-tracker] security binary not found at', SECURITY_BIN);
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
const raw = (0, child_process_1.execSync)(`${SECURITY_BIN} find-generic-password -s "${KEYCHAIN_SERVICE}" -w`, { encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
334
|
+
return parseKeychainToken(raw);
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
338
|
+
if (msg.includes('could not be found')) {
|
|
339
|
+
console.log('[usage-tracker] Keychain: no Claude Code credentials entry found');
|
|
340
|
+
}
|
|
341
|
+
else if (msg.includes('User interaction is not allowed') || msg.includes('errSecInteractionNotAllowed')) {
|
|
342
|
+
console.log('[usage-tracker] Keychain: interaction not allowed, will try osascript fallback');
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
console.log('[usage-tracker] Keychain read failed:', msg.slice(0, 150));
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const OSASCRIPT_BIN = '/usr/bin/osascript';
|
|
351
|
+
function readKeychainViaOsascript() {
|
|
352
|
+
try {
|
|
353
|
+
if (!fs.existsSync(OSASCRIPT_BIN))
|
|
354
|
+
return null;
|
|
355
|
+
const raw = (0, child_process_1.execSync)(`${OSASCRIPT_BIN} -e 'do shell script "${SECURITY_BIN} find-generic-password -s \\\\"${KEYCHAIN_SERVICE}\\\\" -w"'`, { encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
356
|
+
const token = parseKeychainToken(raw);
|
|
357
|
+
if (token) {
|
|
358
|
+
console.log('[usage-tracker] Token source: macOS Keychain (via osascript)');
|
|
359
|
+
}
|
|
360
|
+
return token;
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
364
|
+
console.log('[usage-tracker] Keychain osascript fallback failed:', msg.slice(0, 150));
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Persist the raw keychain JSON to ~/.claude/.credentials.json so that
|
|
370
|
+
* subsequent reads can use the faster file-based path and avoid keychain
|
|
371
|
+
* permission issues entirely.
|
|
372
|
+
*/
|
|
373
|
+
function persistCredentialsFile(accessToken) {
|
|
374
|
+
try {
|
|
375
|
+
fs.mkdirSync(path.dirname(CREDENTIALS_PATH), { recursive: true });
|
|
376
|
+
const data = JSON.stringify({ claudeAiOauth: { accessToken } }, null, 2);
|
|
377
|
+
fs.writeFileSync(CREDENTIALS_PATH, data, { encoding: 'utf8', mode: 0o600 });
|
|
378
|
+
console.log('[usage-tracker] Persisted credentials to file for future reads');
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
console.log('[usage-tracker] Failed to persist credentials file:', err instanceof Error ? err.message : err);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Linux — try secret-tool (libsecret / GNOME Keyring / KDE Wallet)
|
|
385
|
+
function readAccessTokenFromSecretTool() {
|
|
386
|
+
try {
|
|
387
|
+
const raw = (0, child_process_1.execSync)('secret-tool lookup service "Claude Code-credentials"', { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
388
|
+
if (!raw)
|
|
389
|
+
return null;
|
|
390
|
+
const parsed = JSON.parse(raw);
|
|
391
|
+
return parsed?.claudeAiOauth?.accessToken ?? null;
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function readAccessToken() {
|
|
398
|
+
// 1) Try credentials file (works on all platforms)
|
|
399
|
+
const fromFile = readAccessTokenFromFile();
|
|
400
|
+
if (fromFile) {
|
|
401
|
+
console.log('[usage-tracker] Token source: credentials file');
|
|
402
|
+
return fromFile;
|
|
403
|
+
}
|
|
404
|
+
// 2) macOS: Keychain
|
|
405
|
+
if (process.platform === 'darwin') {
|
|
406
|
+
const fromKeychain = readAccessTokenFromKeychain();
|
|
407
|
+
if (fromKeychain) {
|
|
408
|
+
console.log('[usage-tracker] Token source: macOS Keychain');
|
|
409
|
+
return fromKeychain;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// 3) Linux: secret-tool (libsecret)
|
|
413
|
+
if (process.platform === 'linux') {
|
|
414
|
+
const fromSecret = readAccessTokenFromSecretTool();
|
|
415
|
+
if (fromSecret) {
|
|
416
|
+
console.log('[usage-tracker] Token source: secret-tool (libsecret)');
|
|
417
|
+
return fromSecret;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
console.log('[usage-tracker] No credentials found (tried: file' +
|
|
421
|
+
(process.platform === 'darwin' ? ', keychain' : '') +
|
|
422
|
+
(process.platform === 'linux' ? ', secret-tool' : '') + ')');
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
function readAccessTokenWithRetry() {
|
|
426
|
+
const token = readAccessToken();
|
|
427
|
+
if (token !== null)
|
|
428
|
+
return token;
|
|
429
|
+
// EBUSY retry on Windows — small delay then retry once
|
|
430
|
+
if (process.platform === 'win32') {
|
|
431
|
+
try {
|
|
432
|
+
const start = Date.now();
|
|
433
|
+
while (Date.now() - start < 50) { /* busy wait ~50ms */ }
|
|
434
|
+
return readAccessTokenFromFile();
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
function httpsGet(url, token) {
|
|
443
|
+
return new Promise((resolve, reject) => {
|
|
444
|
+
const urlObj = new URL(url);
|
|
445
|
+
const options = {
|
|
446
|
+
hostname: urlObj.hostname,
|
|
447
|
+
path: urlObj.pathname + urlObj.search,
|
|
448
|
+
method: 'GET',
|
|
449
|
+
headers: {
|
|
450
|
+
'Accept': 'application/json, text/plain, */*',
|
|
451
|
+
'Content-Type': 'application/json',
|
|
452
|
+
'User-Agent': 'claude-code/2.0.32',
|
|
453
|
+
'Authorization': `Bearer ${token}`,
|
|
454
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
const req = https.request(options, (res) => {
|
|
458
|
+
let body = '';
|
|
459
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
460
|
+
res.on('end', () => resolve({ status: res.statusCode ?? 0, body }));
|
|
461
|
+
});
|
|
462
|
+
req.on('error', reject);
|
|
463
|
+
req.setTimeout(10_000, () => {
|
|
464
|
+
req.destroy(new Error('Request timed out'));
|
|
465
|
+
});
|
|
466
|
+
req.end();
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
function noData(error, rateLimited = false) {
|
|
470
|
+
return {
|
|
471
|
+
weeklyUtilization: null,
|
|
472
|
+
fiveHourUtilization: null,
|
|
473
|
+
resetsAt: null, fiveHourResetsAt: null,
|
|
474
|
+
dataSource: 'none',
|
|
475
|
+
stale: true,
|
|
476
|
+
rateLimited,
|
|
477
|
+
error,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async function fetchUsage(token) {
|
|
481
|
+
const result = await httpsGet('https://api.anthropic.com/api/oauth/usage', token);
|
|
482
|
+
console.log(`[usage-tracker] API response: status=${result.status}, body=${result.body.slice(0, 200)}`);
|
|
483
|
+
if (result.status === 200) {
|
|
484
|
+
const json = JSON.parse(result.body);
|
|
485
|
+
const apiData = {
|
|
486
|
+
weeklyUtilization: json.seven_day?.utilization ?? null,
|
|
487
|
+
fiveHourUtilization: json.five_hour?.utilization ?? null,
|
|
488
|
+
resetsAt: json.seven_day?.resets_at ?? null,
|
|
489
|
+
fiveHourResetsAt: json.five_hour?.resets_at ?? null,
|
|
490
|
+
dataSource: 'api',
|
|
491
|
+
stale: false,
|
|
492
|
+
rateLimited: false,
|
|
493
|
+
error: null,
|
|
494
|
+
};
|
|
495
|
+
// Learn token-to-percent ratio for future JSONL fallback
|
|
496
|
+
updateCalibration(apiData);
|
|
497
|
+
return apiData;
|
|
498
|
+
}
|
|
499
|
+
if (result.status === 401) {
|
|
500
|
+
return { weeklyUtilization: null, fiveHourUtilization: null, resetsAt: null, fiveHourResetsAt: null, dataSource: 'none', stale: true, rateLimited: false, error: '401' };
|
|
501
|
+
}
|
|
502
|
+
if (result.status === 429) {
|
|
503
|
+
return { weeklyUtilization: null, fiveHourUtilization: null, resetsAt: null, fiveHourResetsAt: null, dataSource: 'none', stale: true, rateLimited: true, error: '429' };
|
|
504
|
+
}
|
|
505
|
+
return { weeklyUtilization: null, fiveHourUtilization: null, resetsAt: null, fiveHourResetsAt: null, dataSource: 'none', stale: true, rateLimited: false, error: `HTTP ${result.status}` };
|
|
506
|
+
}
|
|
507
|
+
async function fetchOnce(token) {
|
|
508
|
+
try {
|
|
509
|
+
const data = await fetchUsage(token);
|
|
510
|
+
if (data.error === null)
|
|
511
|
+
return data;
|
|
512
|
+
// 429 (rate limited) → use JSONL session files for better accuracy
|
|
513
|
+
if (data.rateLimited) {
|
|
514
|
+
console.log('[usage-tracker] API 429, falling back to JSONL session parser');
|
|
515
|
+
return estimateUsageFromJsonl();
|
|
516
|
+
}
|
|
517
|
+
// Other API errors → cache fallback
|
|
518
|
+
console.log(`[usage-tracker] API error (${data.error}), no data available`);
|
|
519
|
+
return noData('No data available');
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
console.log(`[usage-tracker] Network error: ${err instanceof Error ? err.message : err}, no data available`);
|
|
523
|
+
return noData('No data available');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const MAX_BACKOFF_ERR_MS = 15 * 60 * 1000; // 15 min cap for other errors
|
|
527
|
+
function addJitter(baseMs) {
|
|
528
|
+
const jitter = baseMs * 0.15;
|
|
529
|
+
return baseMs + (Math.random() * 2 - 1) * jitter;
|
|
530
|
+
}
|
|
531
|
+
class UsageTracker {
|
|
532
|
+
callbacks = [];
|
|
533
|
+
currentState = {
|
|
534
|
+
weeklyUtilization: null,
|
|
535
|
+
fiveHourUtilization: null,
|
|
536
|
+
resetsAt: null, fiveHourResetsAt: null,
|
|
537
|
+
dataSource: 'none',
|
|
538
|
+
stale: false,
|
|
539
|
+
rateLimited: false,
|
|
540
|
+
error: null,
|
|
541
|
+
};
|
|
542
|
+
timer = null;
|
|
543
|
+
backoffLevel = 0;
|
|
544
|
+
onUpdate(callback) {
|
|
545
|
+
this.callbacks.push(callback);
|
|
546
|
+
}
|
|
547
|
+
getCurrentState() {
|
|
548
|
+
return this.currentState;
|
|
549
|
+
}
|
|
550
|
+
start() {
|
|
551
|
+
// Fetch immediately on start
|
|
552
|
+
void this.poll();
|
|
553
|
+
}
|
|
554
|
+
stop() {
|
|
555
|
+
if (this.timer !== null) {
|
|
556
|
+
clearTimeout(this.timer);
|
|
557
|
+
this.timer = null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
scheduleNext(rateLimited, hasError) {
|
|
561
|
+
let interval;
|
|
562
|
+
if (rateLimited) {
|
|
563
|
+
// Retry every 2 min to avoid hammering the rate-limited endpoint
|
|
564
|
+
interval = 2 * 60 * 1000;
|
|
565
|
+
console.log(`[usage-tracker] Rate limited. Retrying in 2m`);
|
|
566
|
+
}
|
|
567
|
+
else if (hasError) {
|
|
568
|
+
this.backoffLevel = Math.min(this.backoffLevel + 1, 2);
|
|
569
|
+
const cap = MAX_BACKOFF_ERR_MS;
|
|
570
|
+
interval = Math.min(POLL_INTERVAL_MS * Math.pow(2, this.backoffLevel), cap);
|
|
571
|
+
console.log(`[usage-tracker] Error. Backoff level ${this.backoffLevel}, next poll in ${Math.round(interval / 1000)}s`);
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
this.backoffLevel = 0;
|
|
575
|
+
interval = POLL_INTERVAL_MS;
|
|
576
|
+
}
|
|
577
|
+
const jittered = addJitter(interval);
|
|
578
|
+
this.timer = setTimeout(() => { void this.poll(); }, jittered);
|
|
579
|
+
}
|
|
580
|
+
async poll() {
|
|
581
|
+
const data = await this.doPoll();
|
|
582
|
+
this.currentState = data;
|
|
583
|
+
for (const cb of this.callbacks) {
|
|
584
|
+
cb(data);
|
|
585
|
+
}
|
|
586
|
+
this.scheduleNext(data.rateLimited, data.error !== null && data.error !== 'NO_CREDENTIALS');
|
|
587
|
+
}
|
|
588
|
+
async doPoll() {
|
|
589
|
+
// Check for credentials
|
|
590
|
+
let token = readAccessTokenWithRetry();
|
|
591
|
+
if (!token) {
|
|
592
|
+
return {
|
|
593
|
+
weeklyUtilization: null,
|
|
594
|
+
fiveHourUtilization: null,
|
|
595
|
+
resetsAt: null, fiveHourResetsAt: null,
|
|
596
|
+
dataSource: 'none',
|
|
597
|
+
stale: false,
|
|
598
|
+
rateLimited: false,
|
|
599
|
+
error: 'NO_CREDENTIALS',
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
let result = await fetchOnce(token);
|
|
603
|
+
// On 401: re-read credentials (Claude Code may have refreshed them) and retry once
|
|
604
|
+
if (result.error === '401') {
|
|
605
|
+
token = readAccessTokenWithRetry();
|
|
606
|
+
if (!token) {
|
|
607
|
+
return {
|
|
608
|
+
weeklyUtilization: null,
|
|
609
|
+
fiveHourUtilization: null,
|
|
610
|
+
resetsAt: null, fiveHourResetsAt: null,
|
|
611
|
+
dataSource: 'none',
|
|
612
|
+
stale: false,
|
|
613
|
+
rateLimited: false,
|
|
614
|
+
error: 'NO_CREDENTIALS',
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
result = await fetchOnce(token);
|
|
618
|
+
if (result.error === '401') {
|
|
619
|
+
return noData('No data available');
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
exports.UsageTracker = UsageTracker;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.updateAutoLaunch = updateAutoLaunch;
|
|
4
|
+
exports.syncAutoLaunch = syncAutoLaunch;
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
6
|
+
const AutoLaunch = require('auto-launch');
|
|
7
|
+
let petAutoLaunch = null;
|
|
8
|
+
function getAutoLaunch() {
|
|
9
|
+
if (petAutoLaunch)
|
|
10
|
+
return petAutoLaunch;
|
|
11
|
+
try {
|
|
12
|
+
petAutoLaunch = new AutoLaunch({ name: 'Claude Pet' });
|
|
13
|
+
return petAutoLaunch;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Fails in dev/non-packaged mode — no Electron app path available
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function updateAutoLaunch(enabled) {
|
|
21
|
+
try {
|
|
22
|
+
const al = getAutoLaunch();
|
|
23
|
+
if (!al)
|
|
24
|
+
return;
|
|
25
|
+
const isEnabled = await al.isEnabled();
|
|
26
|
+
if (enabled && !isEnabled) {
|
|
27
|
+
await al.enable();
|
|
28
|
+
}
|
|
29
|
+
else if (!enabled && isEnabled) {
|
|
30
|
+
await al.disable();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
console.warn('[auto-launch] failed to update:', err);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function syncAutoLaunch(autoStart) {
|
|
38
|
+
await updateAutoLaunch(autoStart);
|
|
39
|
+
}
|