claude-prism 1.2.0 → 1.2.2

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,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 (when available) |
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,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-prism",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "EUDEC methodology framework for AI coding agents — Essence, Understand, Decompose, Execute, Checkpoint.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@ Report format:
28
28
  Lines:
29
29
  1 ⚡ project:branch | Model | 🔋ctx% | HH:MM
30
30
  2 📋 plan name XX%(done/total) | 💾 commit msg (elapsed)
31
- 3 📊 세션 XX%(Xm) │ 주간XX%(요일 HH:MM)
31
+ 3 📊 XX%(Xm) │ Wkly XX%(Day HH:MM)
32
32
  ```
33
33
 
34
34
  ### enable
@@ -18,7 +18,7 @@ When this command is invoked:
18
18
  ```
19
19
  🌈 claude-prism stats
20
20
 
21
- Version: v0.1.0
21
+ Version: v1.2.2
22
22
  Language: ko
23
23
  Plans: 3 file(s)
24
24
  OMC: ✅ detected
@@ -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
- function getPlanUsage() {
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 cachePath = join(homedir(), '.claude', 'plugins', 'oh-my-claudecode', '.usage-cache.json');
32
- if (!existsSync(cachePath)) return null;
33
- const cache = JSON.parse(readFileSync(cachePath, 'utf-8'));
34
- if (cache.error || !cache.data) return null;
35
- const now = new Date();
36
- const { fiveHourPercent, weeklyPercent, fiveHourResetsAt, weeklyResetsAt } = cache.data;
37
- const minutesUntilReset = Math.max(0, Math.round((new Date(fiveHourResetsAt) - now) / 60000));
38
- const weeklyReset = new Date(weeklyResetsAt);
39
- const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
40
- return {
41
- session: fiveHourPercent,
42
- weekly: weeklyPercent,
43
- sessionResetMin: minutesUntilReset,
44
- weeklyResetLabel: `${dayNames[weeklyReset.getDay()]} ${String(weeklyReset.getHours()).padStart(2, '0')}:${String(weeklyReset.getMinutes()).padStart(2, '0')}`
45
- };
46
- } catch { return null; }
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 = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
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;
@@ -180,7 +288,7 @@ try {
180
288
  const line3 = [];
181
289
  if (planUsage) {
182
290
  const warn = (planUsage.session > 95 || planUsage.weekly > 95) ? '\uD83D\uDD34' : (planUsage.session > 80 || planUsage.weekly > 80) ? '\u26A0\uFE0F' : '';
183
- line3.push(`\uD83D\uDCCA ${planUsage.session}%(${planUsage.sessionResetMin}m) \u2502 \uC8FC\uAC04${planUsage.weekly}%(${planUsage.weeklyResetLabel})${warn}`);
291
+ line3.push(`\uD83D\uDCCA ${planUsage.session}%(${planUsage.sessionResetMin}m) \u2502 Wkly ${planUsage.weekly}%(${planUsage.weeklyResetLabel})${warn}`);
184
292
  }
185
293
 
186
294
  // ── Compose ──