claude-prism 1.1.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 CHANGED
@@ -108,14 +108,16 @@ Prism includes an optional statusline HUD for Claude Code that shows live projec
108
108
  ```
109
109
  ⚑ my-project:main | Opus 4.6 | πŸ”‹84% | 11:17
110
110
  πŸ“‹ auth-refactor 60%(6/10) | πŸ’Ύ fix: token validation (2h)
111
- πŸ“Š μ„Έμ…˜ 45%(30m) β”‚ μ£Όκ°„92%(λͺ© 19:00)
111
+ πŸ“Š 45%(30m) β”‚ Wkly 93%(Wed 19:00)
112
112
  ```
113
113
 
114
114
  | Line | Content |
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
 
@@ -127,7 +129,7 @@ prism hud # Show current status
127
129
 
128
130
  Or from within Claude Code: `/claude-prism:hud enable`
129
131
 
130
- ### 4. Analytics
132
+ ### 5. Analytics
131
133
 
132
134
  Hook events (blocks, warnings) are automatically logged to session files. View aggregated statistics:
133
135
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-prism",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "EUDEC methodology framework for AI coding agents β€” Essence, Understand, Decompose, Execute, Checkpoint.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.1
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 = ['일', 'μ›”', 'ν™”', '수', 'λͺ©', '금', 'ν† '];
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;