claude-prism 1.2.0 → 1.2.1
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 +3 -1
- package/package.json +1 -1
- package/templates/commands/claude-prism/stats.md +1 -1
- package/templates/hud/omc-hud.mjs +128 -20
package/README.md
CHANGED
|
@@ -115,7 +115,9 @@ Prism includes an optional statusline HUD for Claude Code that shows live projec
|
|
|
115
115
|
|------|---------|
|
|
116
116
|
| 1 | Project:branch · model · context % · time |
|
|
117
117
|
| 2 | Active plan progress · last commit · test status |
|
|
118
|
-
| 3 | Session and weekly usage (
|
|
118
|
+
| 3 | Session and weekly usage (auto-refreshed every 30s via Anthropic OAuth API) |
|
|
119
|
+
|
|
120
|
+
The HUD fetches usage data directly from the Anthropic API using your OAuth credentials (macOS Keychain or `~/.claude/.credentials.json`). Results are cached for 30 seconds to minimize API calls.
|
|
119
121
|
|
|
120
122
|
Enable during install (interactive prompt) or at any time:
|
|
121
123
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execSync } from 'child_process';
|
|
3
|
-
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync } from 'fs';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
5
|
import { homedir, tmpdir } from 'os';
|
|
6
|
+
import https from 'https';
|
|
6
7
|
|
|
7
8
|
function shortenModelName(name) {
|
|
8
9
|
return name
|
|
@@ -26,24 +27,131 @@ function hasGitChanges(cwd) {
|
|
|
26
27
|
} catch { return false; }
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
// ── Usage: Cache + API fetch ──
|
|
31
|
+
|
|
32
|
+
const USAGE_CACHE_TTL_MS = 30_000;
|
|
33
|
+
const USAGE_CACHE_TTL_FAIL_MS = 15_000;
|
|
34
|
+
|
|
35
|
+
function getUsageCachePath() {
|
|
36
|
+
return join(homedir(), '.claude', 'plugins', 'oh-my-claudecode', '.usage-cache.json');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isCacheFresh(cache) {
|
|
40
|
+
const ttl = cache.error ? USAGE_CACHE_TTL_FAIL_MS : USAGE_CACHE_TTL_MS;
|
|
41
|
+
return Date.now() - cache.timestamp < ttl;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readCredentials() {
|
|
45
|
+
if (process.platform === 'darwin') {
|
|
46
|
+
try {
|
|
47
|
+
const raw = execSync(
|
|
48
|
+
'/usr/bin/security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null',
|
|
49
|
+
{ encoding: 'utf-8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] }
|
|
50
|
+
).trim();
|
|
51
|
+
if (raw) {
|
|
52
|
+
const creds = JSON.parse(raw);
|
|
53
|
+
const obj = creds.claudeAiOauth || creds;
|
|
54
|
+
if (obj.accessToken) return obj;
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
30
58
|
try {
|
|
31
|
-
const
|
|
32
|
-
if (!existsSync(
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
const p = join(homedir(), '.claude', '.credentials.json');
|
|
60
|
+
if (!existsSync(p)) return null;
|
|
61
|
+
const creds = JSON.parse(readFileSync(p, 'utf-8'));
|
|
62
|
+
const obj = creds.claudeAiOauth || creds;
|
|
63
|
+
if (obj.accessToken) return obj;
|
|
64
|
+
} catch {}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function fetchUsageApi(token) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
const req = https.request({
|
|
71
|
+
hostname: 'api.anthropic.com',
|
|
72
|
+
path: '/api/oauth/usage',
|
|
73
|
+
method: 'GET',
|
|
74
|
+
headers: { Authorization: `Bearer ${token}`, 'anthropic-beta': 'oauth-2025-04-20', 'Content-Type': 'application/json' },
|
|
75
|
+
timeout: 5000,
|
|
76
|
+
}, (res) => {
|
|
77
|
+
let d = '';
|
|
78
|
+
res.on('data', c => d += c);
|
|
79
|
+
res.on('end', () => {
|
|
80
|
+
if (res.statusCode === 200) { try { resolve(JSON.parse(d)); } catch { resolve(null); } }
|
|
81
|
+
else resolve(null);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
req.on('error', () => resolve(null));
|
|
85
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
86
|
+
req.end();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeUsageCache(data, error = false) {
|
|
91
|
+
try {
|
|
92
|
+
const p = getUsageCachePath();
|
|
93
|
+
const dir = dirname(p);
|
|
94
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
95
|
+
writeFileSync(p, JSON.stringify({ timestamp: Date.now(), data, error }, null, 2));
|
|
96
|
+
} catch {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatUsageData(data) {
|
|
100
|
+
const now = new Date();
|
|
101
|
+
const minutesUntilReset = data.fiveHourResetsAt
|
|
102
|
+
? Math.max(0, Math.round((new Date(data.fiveHourResetsAt) - now) / 60000))
|
|
103
|
+
: 0;
|
|
104
|
+
const weeklyReset = data.weeklyResetsAt ? new Date(data.weeklyResetsAt) : null;
|
|
105
|
+
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
|
106
|
+
return {
|
|
107
|
+
session: data.fiveHourPercent,
|
|
108
|
+
weekly: data.weeklyPercent,
|
|
109
|
+
sessionResetMin: minutesUntilReset,
|
|
110
|
+
weeklyResetLabel: weeklyReset
|
|
111
|
+
? `${dayNames[weeklyReset.getDay()]} ${String(weeklyReset.getHours()).padStart(2, '0')}:${String(weeklyReset.getMinutes()).padStart(2, '0')}`
|
|
112
|
+
: '--:--',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function getPlanUsage() {
|
|
117
|
+
const cachePath = getUsageCachePath();
|
|
118
|
+
let cache = null;
|
|
119
|
+
try {
|
|
120
|
+
if (existsSync(cachePath)) {
|
|
121
|
+
cache = JSON.parse(readFileSync(cachePath, 'utf-8'));
|
|
122
|
+
if (cache && !cache.error && cache.data && isCacheFresh(cache)) {
|
|
123
|
+
return formatUsageData(cache.data);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch {}
|
|
127
|
+
|
|
128
|
+
// Cache stale — fetch from Anthropic API
|
|
129
|
+
const creds = readCredentials();
|
|
130
|
+
if (creds?.accessToken && (!creds.expiresAt || creds.expiresAt > Date.now())) {
|
|
131
|
+
const resp = await fetchUsageApi(creds.accessToken);
|
|
132
|
+
if (resp) {
|
|
133
|
+
const clamp = v => (v == null || !isFinite(v)) ? 0 : Math.max(0, Math.min(100, v));
|
|
134
|
+
const fh = resp.five_hour?.utilization;
|
|
135
|
+
const sd = resp.seven_day?.utilization;
|
|
136
|
+
if (fh != null || sd != null) {
|
|
137
|
+
const data = {
|
|
138
|
+
fiveHourPercent: clamp(fh),
|
|
139
|
+
weeklyPercent: clamp(sd),
|
|
140
|
+
fiveHourResetsAt: resp.five_hour?.resets_at || null,
|
|
141
|
+
weeklyResetsAt: resp.seven_day?.resets_at || null,
|
|
142
|
+
sonnetWeeklyPercent: resp.seven_day_sonnet?.utilization != null ? clamp(resp.seven_day_sonnet.utilization) : undefined,
|
|
143
|
+
sonnetWeeklyResetsAt: resp.seven_day_sonnet?.resets_at || undefined,
|
|
144
|
+
};
|
|
145
|
+
writeUsageCache(data);
|
|
146
|
+
return formatUsageData(data);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
writeUsageCache(null, true);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fallback: return stale cache if available
|
|
153
|
+
if (cache?.data) return formatUsageData(cache.data);
|
|
154
|
+
return null;
|
|
47
155
|
}
|
|
48
156
|
|
|
49
157
|
function getGitRoot(cwd) {
|
|
@@ -129,7 +237,7 @@ try {
|
|
|
129
237
|
const dirName = cwd.split('/').pop();
|
|
130
238
|
const modelName = shortenModelName(context.model?.display_name || 'Claude');
|
|
131
239
|
const remaining = context.context_window?.remaining_percentage;
|
|
132
|
-
const planUsage = getPlanUsage();
|
|
240
|
+
const planUsage = await getPlanUsage();
|
|
133
241
|
|
|
134
242
|
let gitBranch = '';
|
|
135
243
|
let gitDirty = false;
|