@triflux/core 10.1.0 → 10.25.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.
Files changed (143) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +20 -8
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +267 -35
  7. package/hooks/hook-registry.json +343 -256
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +183 -23
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +254 -33
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +831 -290
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -4
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +19 -23
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/service.mjs +88 -64
  35. package/hub/delegator/tool-definitions.mjs +35 -35
  36. package/hub/dynamic-routing-engine.mjs +511 -0
  37. package/hub/fullcycle.mjs +34 -18
  38. package/hub/gemini-adapter.mjs +77 -95
  39. package/hub/hitl.mjs +202 -143
  40. package/hub/intent.mjs +167 -38
  41. package/hub/lib/bash-path.mjs +73 -0
  42. package/hub/lib/cache-guard.mjs +43 -17
  43. package/hub/lib/env-detect.mjs +145 -0
  44. package/hub/lib/hosts-compat.mjs +319 -0
  45. package/hub/lib/mcp-response-cache.mjs +239 -0
  46. package/hub/lib/memory-store.mjs +285 -111
  47. package/hub/lib/path-utils.mjs +165 -0
  48. package/hub/lib/phase-manager.mjs +493 -0
  49. package/hub/lib/process-utils.mjs +797 -104
  50. package/hub/lib/prompt-tmp.mjs +33 -0
  51. package/hub/lib/risk-tier.mjs +53 -0
  52. package/hub/lib/spawn-trace.mjs +561 -0
  53. package/hub/lib/ssh-command.mjs +44 -24
  54. package/hub/lib/ssh-retry.mjs +5 -1
  55. package/hub/lib/staged-file-detect.mjs +43 -0
  56. package/hub/lib/state-snapshot.mjs +293 -0
  57. package/hub/lib/tfx-route-args.mjs +309 -0
  58. package/hub/lib/timeline-adapter.mjs +190 -0
  59. package/hub/lib/trace-recorder.mjs +153 -0
  60. package/hub/lib/uuidv7.mjs +4 -3
  61. package/hub/memory-doctor.mjs +266 -106
  62. package/hub/middleware/quota-middleware.mjs +70 -0
  63. package/hub/middleware/request-logger.mjs +61 -34
  64. package/hub/paths.mjs +9 -9
  65. package/hub/pipeline/gates/confidence.mjs +34 -15
  66. package/hub/pipeline/gates/consensus.mjs +27 -15
  67. package/hub/pipeline/gates/index.mjs +7 -3
  68. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  69. package/hub/pipeline/index.mjs +77 -42
  70. package/hub/pipeline/state.mjs +10 -10
  71. package/hub/pipeline/transitions.mjs +40 -23
  72. package/hub/platform.mjs +65 -48
  73. package/hub/quality/deslop.mjs +70 -49
  74. package/hub/reflexion.mjs +87 -13
  75. package/hub/research.mjs +33 -25
  76. package/hub/router.mjs +941 -791
  77. package/hub/routing/complexity.mjs +132 -29
  78. package/hub/routing/index.mjs +17 -12
  79. package/hub/routing/q-learning.mjs +76 -28
  80. package/hub/routing-snapshot.mjs +261 -0
  81. package/hub/session-fingerprint.mjs +127 -61
  82. package/hub/state.mjs +86 -44
  83. package/hub/team/retry-state-machine.mjs +267 -0
  84. package/hub/token-mode.mjs +72 -71
  85. package/hub/workers/worker-utils.mjs +22 -16
  86. package/hud/colors.mjs +27 -9
  87. package/hud/constants.mjs +184 -27
  88. package/hud/context-monitor.mjs +133 -44
  89. package/hud/hud-qos-status.mjs +153 -50
  90. package/hud/mission-board.mjs +56 -0
  91. package/hud/providers/claude.mjs +398 -114
  92. package/hud/providers/codex.mjs +96 -21
  93. package/hud/providers/gemini.mjs +184 -51
  94. package/hud/renderers.mjs +413 -123
  95. package/hud/terminal.mjs +101 -31
  96. package/hud/utils.mjs +81 -39
  97. package/mesh/index.mjs +69 -0
  98. package/mesh/mesh-budget.mjs +137 -0
  99. package/mesh/mesh-heartbeat.mjs +100 -0
  100. package/mesh/mesh-protocol.mjs +96 -0
  101. package/mesh/mesh-queue.mjs +163 -0
  102. package/mesh/mesh-registry.mjs +78 -0
  103. package/mesh/mesh-router.mjs +77 -0
  104. package/package.json +5 -1
  105. package/scripts/lib/agent-json.mjs +27 -0
  106. package/scripts/lib/async.mjs +174 -0
  107. package/scripts/lib/claudemd-scanner.mjs +6 -1
  108. package/scripts/lib/cli-agy.mjs +62 -0
  109. package/scripts/lib/cli-claude.mjs +78 -0
  110. package/scripts/lib/cli-codex.mjs +199 -0
  111. package/scripts/lib/cli-gemini.mjs +67 -0
  112. package/scripts/lib/codex-recovery.sh +50 -0
  113. package/scripts/lib/context.mjs +3 -3
  114. package/scripts/lib/cross-review-utils.mjs +13 -5
  115. package/scripts/lib/doctor-env-checks.mjs +121 -0
  116. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  117. package/scripts/lib/env-probe.mjs +58 -30
  118. package/scripts/lib/env.mjs +14 -0
  119. package/scripts/lib/gemini-profiles.mjs +44 -10
  120. package/scripts/lib/handoff.mjs +33 -17
  121. package/scripts/lib/hook-utils.mjs +8 -6
  122. package/scripts/lib/hub.mjs +48 -0
  123. package/scripts/lib/keyword-rules.mjs +48 -19
  124. package/scripts/lib/logger.mjs +24 -24
  125. package/scripts/lib/mcp-filter.mjs +391 -247
  126. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  127. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  128. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  129. package/scripts/lib/mcp-guard-engine.mjs +1281 -115
  130. package/scripts/lib/mcp-health.mjs +773 -0
  131. package/scripts/lib/mcp-manifest.mjs +24 -14
  132. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  133. package/scripts/lib/pid.mjs +63 -0
  134. package/scripts/lib/process-utils.mjs +26 -0
  135. package/scripts/lib/psmux-info.mjs +70 -10
  136. package/scripts/lib/quota.mjs +47 -0
  137. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  138. package/scripts/lib/skill-state.mjs +220 -0
  139. package/scripts/lib/skill-template.mjs +32 -7
  140. package/scripts/lib/team.mjs +75 -0
  141. package/scripts/lib/timeout.mjs +66 -0
  142. package/scripts/lib/tmp.mjs +26 -0
  143. package/scripts/lib/toml.mjs +113 -0
@@ -1,20 +1,44 @@
1
1
  // ============================================================================
2
2
  // Claude Usage API (api.anthropic.com/api/oauth/usage)
3
3
  // ============================================================================
4
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
- import { homedir } from "node:os";
6
- import { join } from "node:path";
4
+
5
+ import { execFileSync, spawn } from "node:child_process";
6
+ import { existsSync, writeFileSync } from "node:fs";
7
7
  import https from "node:https";
8
- import { spawn } from "node:child_process";
9
8
  import {
10
- CLAUDE_CREDENTIALS_PATH, CLAUDE_USAGE_CACHE_PATH, OMC_PLUGIN_USAGE_CACHE_PATH,
11
- CLAUDE_USAGE_STALE_MS_SOLO, CLAUDE_USAGE_STALE_MS_WITH_OMC,
12
- CLAUDE_USAGE_429_BACKOFF_MS, CLAUDE_USAGE_ERROR_BACKOFF_MS,
13
- CLAUDE_API_TIMEOUT_MS, FIVE_HOUR_MS, SEVEN_DAY_MS,
14
- DEFAULT_OAUTH_CLIENT_ID, CLAUDE_REFRESH_FLAG,
9
+ CLAUDE_API_TIMEOUT_MS,
10
+ CLAUDE_CREDENTIALS_PATH,
11
+ CLAUDE_REFRESH_FLAG,
12
+ CLAUDE_REFRESH_LOCK_PATH,
13
+ CLAUDE_USAGE_429_BACKOFF_MS,
14
+ CLAUDE_USAGE_CACHE_PATH,
15
+ CLAUDE_USAGE_ERROR_BACKOFF_MS,
16
+ CLAUDE_USAGE_STALE_MS_SOLO,
17
+ CLAUDE_USAGE_STALE_MS_WITH_OMC,
18
+ DEFAULT_OAUTH_CLIENT_ID,
19
+ FIVE_HOUR_MS,
20
+ OMC_PLUGIN_USAGE_CACHE_PATH,
21
+ SEVEN_DAY_MS,
22
+ SPAWN_LOCK_TTL_MS,
15
23
  } from "../constants.mjs";
16
- import { readJson, writeJsonSafe, clampPercent, advanceToNextCycle } from "../utils.mjs";
17
24
  import { readContextMonitorSnapshot } from "../context-monitor.mjs";
25
+ import {
26
+ advanceToNextCycle,
27
+ clampPercent,
28
+ readJson,
29
+ writeJsonSafe,
30
+ } from "../utils.mjs";
31
+
32
+ export const CLAUDE_USAGE_POLL_BASE_MS = 5_000;
33
+ export const CLAUDE_USAGE_POLL_JITTER_RATIO = 0.2;
34
+ const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
35
+ export const CLAUDE_USAGE_RATE_LIMIT_BACKOFF_MS = [
36
+ CLAUDE_USAGE_POLL_BASE_MS,
37
+ 10_000,
38
+ 30_000,
39
+ 60_000,
40
+ 120_000,
41
+ ];
18
42
 
19
43
  // OMC 활성 여부에 따라 캐시 TTL 동적 결정
20
44
  function getClaudeUsageStaleMs() {
@@ -23,8 +47,59 @@ function getClaudeUsageStaleMs() {
23
47
  : CLAUDE_USAGE_STALE_MS_SOLO;
24
48
  }
25
49
 
26
- export function readClaudeCredentials() {
27
- const data = readJson(CLAUDE_CREDENTIALS_PATH, null);
50
+ export function computeClaudeUsagePollState({
51
+ consecutive429s = 0,
52
+ outcome = "success",
53
+ random = Math.random,
54
+ jitterRatio = CLAUDE_USAGE_POLL_JITTER_RATIO,
55
+ } = {}) {
56
+ const current429s = Number.isFinite(consecutive429s)
57
+ ? Math.max(0, consecutive429s)
58
+ : 0;
59
+ const next429s = outcome === "rate_limit" ? current429s + 1 : 0;
60
+ const stepIndex =
61
+ outcome === "rate_limit"
62
+ ? Math.min(next429s, CLAUDE_USAGE_RATE_LIMIT_BACKOFF_MS.length - 1)
63
+ : 0;
64
+ const baseDelayMs = CLAUDE_USAGE_RATE_LIMIT_BACKOFF_MS[stepIndex];
65
+ const sample = Number(random?.());
66
+ const normalized = Number.isFinite(sample)
67
+ ? Math.min(1, Math.max(0, sample))
68
+ : 0.5;
69
+ const jitterFactor = 1 + (normalized * 2 - 1) * jitterRatio;
70
+ return {
71
+ consecutive429s: next429s,
72
+ baseDelayMs,
73
+ delayMs: Math.max(1, Math.round(baseDelayMs * jitterFactor)),
74
+ };
75
+ }
76
+
77
+ function getSnapshotSchedule(cache) {
78
+ const timestamp = Number(cache?.timestamp);
79
+ const nextRefreshAt = Number(cache?.nextRefreshAt);
80
+ if (Number.isFinite(nextRefreshAt)) {
81
+ return {
82
+ nextRefreshAt,
83
+ shouldRefresh: Date.now() >= nextRefreshAt,
84
+ };
85
+ }
86
+
87
+ const ageMs = Number.isFinite(timestamp)
88
+ ? Date.now() - timestamp
89
+ : Number.MAX_SAFE_INTEGER;
90
+ const fallbackMs = cache?.error
91
+ ? cache.errorType === "rate_limit"
92
+ ? CLAUDE_USAGE_429_BACKOFF_MS
93
+ : CLAUDE_USAGE_ERROR_BACKOFF_MS
94
+ : getClaudeUsageStaleMs();
95
+
96
+ return {
97
+ nextRefreshAt: Number.isFinite(timestamp) ? timestamp + fallbackMs : null,
98
+ shouldRefresh: ageMs >= fallbackMs,
99
+ };
100
+ }
101
+
102
+ function normalizeClaudeCredentials(data, source, supportsUsageApi = true) {
28
103
  if (!data) return null;
29
104
  const creds = data.claudeAiOauth || data;
30
105
  if (!creds.accessToken) return null;
@@ -32,91 +107,208 @@ export function readClaudeCredentials() {
32
107
  accessToken: creds.accessToken,
33
108
  refreshToken: creds.refreshToken,
34
109
  expiresAt: creds.expiresAt,
110
+ source,
111
+ supportsUsageApi,
35
112
  };
36
113
  }
37
114
 
115
+ function readClaudeKeychainCredentials(execFileSyncFn = execFileSync) {
116
+ try {
117
+ const raw = execFileSyncFn(
118
+ "security",
119
+ ["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-w"],
120
+ { encoding: "utf8" },
121
+ );
122
+ return normalizeClaudeCredentials(JSON.parse(raw.trim()), "keychain");
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ export function readClaudeCredentials({
129
+ readCredentialFile = () => readJson(CLAUDE_CREDENTIALS_PATH, null),
130
+ platform = process.platform,
131
+ execFileSyncFn = execFileSync,
132
+ env = process.env,
133
+ } = {}) {
134
+ const fileCreds = normalizeClaudeCredentials(readCredentialFile(), "file");
135
+ if (fileCreds) return fileCreds;
136
+
137
+ if (platform === "darwin") {
138
+ const keychainCreds = readClaudeKeychainCredentials(execFileSyncFn);
139
+ if (keychainCreds) return keychainCreds;
140
+ }
141
+
142
+ if (env.ANTHROPIC_API_KEY) {
143
+ return {
144
+ accessToken: env.ANTHROPIC_API_KEY,
145
+ refreshToken: null,
146
+ expiresAt: null,
147
+ source: "env",
148
+ supportsUsageApi: false,
149
+ };
150
+ }
151
+
152
+ return null;
153
+ }
154
+
38
155
  export function refreshClaudeAccessToken(refreshToken) {
39
156
  return new Promise((resolve) => {
40
- const clientId = process.env.CLAUDE_CODE_OAUTH_CLIENT_ID || DEFAULT_OAUTH_CLIENT_ID;
157
+ const clientId =
158
+ process.env.CLAUDE_CODE_OAUTH_CLIENT_ID || DEFAULT_OAUTH_CLIENT_ID;
41
159
  const body = new URLSearchParams({
42
160
  grant_type: "refresh_token",
43
161
  refresh_token: refreshToken,
44
162
  client_id: clientId,
45
163
  }).toString();
46
- const req = https.request({
47
- hostname: "platform.claude.com",
48
- path: "/v1/oauth/token",
49
- method: "POST",
50
- headers: {
51
- "Content-Type": "application/x-www-form-urlencoded",
52
- "Content-Length": Buffer.byteLength(body),
164
+ const req = https.request(
165
+ {
166
+ hostname: "platform.claude.com",
167
+ path: "/v1/oauth/token",
168
+ method: "POST",
169
+ headers: {
170
+ "Content-Type": "application/x-www-form-urlencoded",
171
+ "Content-Length": Buffer.byteLength(body),
172
+ },
173
+ timeout: CLAUDE_API_TIMEOUT_MS,
53
174
  },
54
- timeout: CLAUDE_API_TIMEOUT_MS,
55
- }, (res) => {
56
- let data = "";
57
- res.on("data", (chunk) => { data += chunk; });
58
- res.on("end", () => {
59
- if (res.statusCode === 200) {
60
- try {
61
- const parsed = JSON.parse(data);
62
- if (parsed.access_token) {
63
- resolve({
64
- accessToken: parsed.access_token,
65
- refreshToken: parsed.refresh_token || refreshToken,
66
- expiresAt: parsed.expires_in
67
- ? Date.now() + parsed.expires_in * 1000
68
- : parsed.expires_at,
69
- });
70
- return;
175
+ (res) => {
176
+ let data = "";
177
+ res.on("data", (chunk) => {
178
+ data += chunk;
179
+ });
180
+ res.on("end", () => {
181
+ if (res.statusCode === 200) {
182
+ try {
183
+ const parsed = JSON.parse(data);
184
+ if (parsed.access_token) {
185
+ resolve({
186
+ accessToken: parsed.access_token,
187
+ refreshToken: parsed.refresh_token || refreshToken,
188
+ expiresAt: parsed.expires_in
189
+ ? Date.now() + parsed.expires_in * 1000
190
+ : parsed.expires_at,
191
+ });
192
+ return;
193
+ }
194
+ } catch {
195
+ /* parse 실패 */
71
196
  }
72
- } catch { /* parse 실패 */ }
73
- }
74
- resolve(null);
75
- });
76
- });
197
+ }
198
+ resolve(null);
199
+ });
200
+ },
201
+ );
77
202
  req.on("error", () => resolve(null));
78
- req.on("timeout", () => { req.destroy(); resolve(null); });
203
+ req.on("timeout", () => {
204
+ req.destroy();
205
+ resolve(null);
206
+ });
79
207
  req.end(body);
80
208
  });
81
209
  }
82
210
 
83
- export function writeBackClaudeCredentials(creds) {
211
+ function serializeClaudeCredentials(creds) {
212
+ const oauth = {
213
+ accessToken: creds.accessToken,
214
+ };
215
+ if (creds.expiresAt != null) oauth.expiresAt = creds.expiresAt;
216
+ if (creds.refreshToken) oauth.refreshToken = creds.refreshToken;
217
+ return { claudeAiOauth: oauth };
218
+ }
219
+
220
+ function writeClaudeKeychainCredentials(creds, execFileSyncFn = execFileSync) {
221
+ execFileSyncFn(
222
+ "security",
223
+ [
224
+ "add-generic-password",
225
+ "-s",
226
+ CLAUDE_KEYCHAIN_SERVICE,
227
+ "-w",
228
+ JSON.stringify(serializeClaudeCredentials(creds)),
229
+ "-U",
230
+ ],
231
+ { stdio: "ignore" },
232
+ );
233
+ }
234
+
235
+ export function writeBackClaudeCredentials(
236
+ creds,
237
+ {
238
+ readCredentialFile = () => readJson(CLAUDE_CREDENTIALS_PATH, null),
239
+ writeCredentialFile = (data) =>
240
+ writeFileSync(CLAUDE_CREDENTIALS_PATH, JSON.stringify(data, null, 2)),
241
+ platform = process.platform,
242
+ execFileSyncFn = execFileSync,
243
+ } = {},
244
+ ) {
245
+ if (creds?.source === "env") return;
246
+
247
+ let data = null;
84
248
  try {
85
- const data = readJson(CLAUDE_CREDENTIALS_PATH, null);
86
- if (!data) return;
249
+ data = readCredentialFile();
250
+ } catch {
251
+ data = null;
252
+ }
253
+
254
+ if (data) {
87
255
  const target = data.claudeAiOauth || data;
88
256
  target.accessToken = creds.accessToken;
89
257
  if (creds.expiresAt != null) target.expiresAt = creds.expiresAt;
90
258
  if (creds.refreshToken) target.refreshToken = creds.refreshToken;
91
- writeFileSync(CLAUDE_CREDENTIALS_PATH, JSON.stringify(data, null, 2));
92
- } catch { /* 쓰기 실패 무시 */ }
259
+ try {
260
+ writeCredentialFile(data);
261
+ } catch {
262
+ /* 파일 쓰기 실패 무시 */
263
+ }
264
+ }
265
+
266
+ if (platform === "darwin") {
267
+ try {
268
+ writeClaudeKeychainCredentials(creds, execFileSyncFn);
269
+ } catch {
270
+ /* Keychain 쓰기 실패 무시 */
271
+ }
272
+ }
93
273
  }
94
274
 
95
275
  export function fetchClaudeUsageFromApi(accessToken) {
96
276
  return new Promise((resolve) => {
97
- const req = https.request({
98
- hostname: "api.anthropic.com",
99
- path: "/api/oauth/usage",
100
- method: "GET",
101
- headers: {
102
- "Authorization": `Bearer ${accessToken}`,
103
- "anthropic-beta": "oauth-2025-04-20",
104
- "Content-Type": "application/json",
277
+ const req = https.request(
278
+ {
279
+ hostname: "api.anthropic.com",
280
+ path: "/api/oauth/usage",
281
+ method: "GET",
282
+ headers: {
283
+ Authorization: `Bearer ${accessToken}`,
284
+ "anthropic-beta": "oauth-2025-04-20",
285
+ "Content-Type": "application/json",
286
+ },
287
+ timeout: CLAUDE_API_TIMEOUT_MS,
105
288
  },
106
- timeout: CLAUDE_API_TIMEOUT_MS,
107
- }, (res) => {
108
- let data = "";
109
- res.on("data", (chunk) => { data += chunk; });
110
- res.on("end", () => {
111
- if (res.statusCode === 200) {
112
- try { resolve({ ok: true, data: JSON.parse(data) }); } catch { resolve({ ok: false, status: 0 }); }
113
- } else {
114
- resolve({ ok: false, status: res.statusCode });
115
- }
116
- });
117
- });
289
+ (res) => {
290
+ let data = "";
291
+ res.on("data", (chunk) => {
292
+ data += chunk;
293
+ });
294
+ res.on("end", () => {
295
+ if (res.statusCode === 200) {
296
+ try {
297
+ resolve({ ok: true, data: JSON.parse(data) });
298
+ } catch {
299
+ resolve({ ok: false, status: 0 });
300
+ }
301
+ } else {
302
+ resolve({ ok: false, status: res.statusCode });
303
+ }
304
+ });
305
+ },
306
+ );
118
307
  req.on("error", () => resolve({ ok: false, status: 0, error: "network" }));
119
- req.on("timeout", () => { req.destroy(); resolve({ ok: false, status: 0, error: "timeout" }); });
308
+ req.on("timeout", () => {
309
+ req.destroy();
310
+ resolve({ ok: false, status: 0, error: "timeout" });
311
+ });
120
312
  req.end();
121
313
  });
122
314
  }
@@ -125,12 +317,16 @@ export function parseClaudeUsageResponse(response) {
125
317
  if (!response || typeof response !== "object") return null;
126
318
  // five_hour/seven_day 키 자체가 없으면 비정상 응답
127
319
  if (!response.five_hour && !response.seven_day) return null;
128
- const fiveHour = response.five_hour?.utilization;
129
- const sevenDay = response.seven_day?.utilization;
130
- // utilization이 null이면 0%로 처리 (API 200 성공 시 null = 사용량 없음)
320
+ // 키 자체가 부재하면 percent=null (HUD --% placeholder), utilization=null 만 0%로 처리
321
+ const fiveHourPresent = response.five_hour != null;
322
+ const sevenDayPresent = response.seven_day != null;
131
323
  return {
132
- fiveHourPercent: clampPercent(fiveHour ?? 0),
133
- weeklyPercent: clampPercent(sevenDay ?? 0),
324
+ fiveHourPercent: fiveHourPresent
325
+ ? clampPercent(response.five_hour.utilization ?? 0)
326
+ : null,
327
+ weeklyPercent: sevenDayPresent
328
+ ? clampPercent(response.seven_day.utilization ?? 0)
329
+ : null,
134
330
  fiveHourResetsAt: response.five_hour?.resets_at || null,
135
331
  weeklyResetsAt: response.seven_day?.resets_at || null,
136
332
  };
@@ -142,11 +338,17 @@ export function stripStaleResets(data) {
142
338
  const copy = { ...data };
143
339
  if (copy.fiveHourResetsAt) {
144
340
  const t = new Date(copy.fiveHourResetsAt).getTime();
145
- if (!isNaN(t)) copy.fiveHourResetsAt = new Date(advanceToNextCycle(t, FIVE_HOUR_MS)).toISOString();
341
+ if (!Number.isNaN(t))
342
+ copy.fiveHourResetsAt = new Date(
343
+ advanceToNextCycle(t, FIVE_HOUR_MS),
344
+ ).toISOString();
146
345
  }
147
346
  if (copy.weeklyResetsAt) {
148
347
  const t = new Date(copy.weeklyResetsAt).getTime();
149
- if (!isNaN(t)) copy.weeklyResetsAt = new Date(advanceToNextCycle(t, SEVEN_DAY_MS)).toISOString();
348
+ if (!Number.isNaN(t))
349
+ copy.weeklyResetsAt = new Date(
350
+ advanceToNextCycle(t, SEVEN_DAY_MS),
351
+ ).toISOString();
150
352
  }
151
353
  return copy;
152
354
  }
@@ -154,45 +356,56 @@ export function stripStaleResets(data) {
154
356
  export function readClaudeUsageSnapshot() {
155
357
  const cache = readJson(CLAUDE_USAGE_CACHE_PATH, null);
156
358
  const ts = Number(cache?.timestamp);
157
- const ageMs = Number.isFinite(ts) ? Date.now() - ts : Number.MAX_SAFE_INTEGER;
359
+ const schedule = getSnapshotSchedule(cache);
360
+ const staleBackoffActive =
361
+ cache?.errorType === "rate_limit" &&
362
+ Number.isFinite(schedule.nextRefreshAt) &&
363
+ Date.now() < schedule.nextRefreshAt;
158
364
 
159
365
  // 1차: 자체 캐시에 유효 데이터가 있는 경우
160
366
  if (cache?.data) {
161
367
  // 에러 상태에서 보존된 stale 데이터 → backoff 존중하되 표시용 데이터 반환
162
368
  if (cache.error) {
163
- const backoffMs = cache.errorType === "rate_limit"
164
- ? CLAUDE_USAGE_429_BACKOFF_MS
165
- : CLAUDE_USAGE_ERROR_BACKOFF_MS;
166
- return { data: stripStaleResets(cache.data), shouldRefresh: ageMs >= backoffMs };
369
+ return {
370
+ data: stripStaleResets(cache.data),
371
+ shouldRefresh: schedule.shouldRefresh,
372
+ isStale: staleBackoffActive,
373
+ };
167
374
  }
168
- const isFresh = ageMs < getClaudeUsageStaleMs();
169
375
  // resets_at이 지난 윈도우의 percent를 0으로 보정 (stale 캐시 방지)
170
376
  const data = { ...cache.data };
171
377
  const now = Date.now();
172
- if (data.fiveHourResetsAt && new Date(data.fiveHourResetsAt).getTime() <= now) {
378
+ if (
379
+ data.fiveHourResetsAt &&
380
+ new Date(data.fiveHourResetsAt).getTime() <= now
381
+ ) {
173
382
  data.fiveHourPercent = 0;
174
383
  }
175
384
  if (data.weeklyResetsAt && new Date(data.weeklyResetsAt).getTime() <= now) {
176
385
  data.weeklyPercent = 0;
177
386
  }
178
- return { data, shouldRefresh: !isFresh };
387
+ return { data, shouldRefresh: schedule.shouldRefresh, isStale: false };
179
388
  }
180
389
 
181
390
  // 2차: 에러 backoff — 최근 에러 시 재시도 억제 (무한 spawn 방지)
182
391
  if (cache?.error && Number.isFinite(ts)) {
183
- const backoffMs = cache.errorType === "rate_limit"
184
- ? CLAUDE_USAGE_429_BACKOFF_MS
185
- : CLAUDE_USAGE_ERROR_BACKOFF_MS;
186
- if (ageMs < backoffMs) {
392
+ if (!schedule.shouldRefresh) {
187
393
  const omcCache = readJson(OMC_PLUGIN_USAGE_CACHE_PATH, null);
188
394
  // OMC 캐시가 에러 이후 갱신되었으면 → 에러 캐시 덮어쓰고 그 데이터 사용
189
395
  if (omcCache?.data?.fiveHourPercent != null && omcCache.timestamp > ts) {
190
396
  writeClaudeUsageCache(omcCache.data);
191
- return { data: omcCache.data, shouldRefresh: false };
397
+ return { data: omcCache.data, shouldRefresh: false, isStale: false };
192
398
  }
193
399
  // stale OMC fallback 또는 null (--% 플레이스홀더 표시, 가짜 0% 방지)
194
- const staleData = omcCache?.data?.fiveHourPercent != null ? stripStaleResets(omcCache.data) : null;
195
- return { data: staleData, shouldRefresh: false };
400
+ const staleData =
401
+ omcCache?.data?.fiveHourPercent != null
402
+ ? stripStaleResets(omcCache.data)
403
+ : null;
404
+ return {
405
+ data: staleData,
406
+ shouldRefresh: false,
407
+ isStale: staleBackoffActive,
408
+ };
196
409
  }
197
410
  }
198
411
 
@@ -200,26 +413,58 @@ export function readClaudeUsageSnapshot() {
200
413
  const OMC_CACHE_MAX_AGE_MS = 30 * 60 * 1000;
201
414
  const omcCache = readJson(OMC_PLUGIN_USAGE_CACHE_PATH, null);
202
415
  if (omcCache?.data?.fiveHourPercent != null) {
203
- const omcAge = Number.isFinite(omcCache.timestamp) ? Date.now() - omcCache.timestamp : Number.MAX_SAFE_INTEGER;
416
+ const omcAge = Number.isFinite(omcCache.timestamp)
417
+ ? Date.now() - omcCache.timestamp
418
+ : Number.MAX_SAFE_INTEGER;
204
419
  if (omcAge < OMC_CACHE_MAX_AGE_MS) {
205
420
  writeClaudeUsageCache(omcCache.data);
206
- return { data: omcCache.data, shouldRefresh: omcAge > getClaudeUsageStaleMs() };
421
+ return {
422
+ data: omcCache.data,
423
+ shouldRefresh: omcAge > getClaudeUsageStaleMs(),
424
+ isStale: false,
425
+ };
207
426
  }
208
427
  // stale이어도 data: null보다는 오래된 데이터를 fallback으로 표시
209
- return { data: stripStaleResets(omcCache.data), shouldRefresh: true };
428
+ return {
429
+ data: stripStaleResets(omcCache.data),
430
+ shouldRefresh: true,
431
+ isStale: false,
432
+ };
210
433
  }
211
434
 
212
435
  // 캐시/fallback 모두 없음: null 반환 → --% 플레이스홀더 + 리프레시 시도
213
- return { data: null, shouldRefresh: true };
436
+ return { data: null, shouldRefresh: true, isStale: false };
214
437
  }
215
438
 
216
- export function writeClaudeUsageCache(data, errorInfo = null) {
439
+ export function writeClaudeUsageCache(
440
+ data,
441
+ errorInfo = null,
442
+ pollState = null,
443
+ ) {
444
+ const state =
445
+ pollState ||
446
+ (errorInfo
447
+ ? {
448
+ consecutive429s: errorInfo.type === "rate_limit" ? 1 : 0,
449
+ baseDelayMs:
450
+ errorInfo.type === "rate_limit"
451
+ ? CLAUDE_USAGE_429_BACKOFF_MS
452
+ : CLAUDE_USAGE_ERROR_BACKOFF_MS,
453
+ delayMs:
454
+ errorInfo.type === "rate_limit"
455
+ ? CLAUDE_USAGE_429_BACKOFF_MS
456
+ : CLAUDE_USAGE_ERROR_BACKOFF_MS,
457
+ }
458
+ : computeClaudeUsagePollState({ outcome: "success" }));
217
459
  const entry = {
218
460
  timestamp: Date.now(),
219
461
  data,
220
462
  error: !!errorInfo,
221
- errorType: errorInfo?.type || null, // "rate_limit" | "auth" | "network" | "unknown"
463
+ errorType: errorInfo?.type || null, // "rate_limit" | "auth" | "network" | "unknown"
222
464
  errorStatus: errorInfo?.status || null, // HTTP 상태 코드
465
+ consecutive429s: state.consecutive429s,
466
+ nextRefreshBaseMs: state.baseDelayMs,
467
+ nextRefreshAt: Date.now() + state.delayMs,
223
468
  };
224
469
  // 에러 시 기존 유효 데이터 보존 (--% n/a 방지)
225
470
  if (errorInfo && data == null) {
@@ -234,14 +479,22 @@ export function writeClaudeUsageCache(data, errorInfo = null) {
234
479
 
235
480
  export async function fetchClaudeUsage(forceRefresh = false) {
236
481
  const existingSnapshot = readClaudeUsageSnapshot();
237
- if (!forceRefresh && !existingSnapshot.shouldRefresh && existingSnapshot.data) {
238
- return existingSnapshot.data;
482
+ if (!forceRefresh && !existingSnapshot.shouldRefresh) {
483
+ return existingSnapshot.data || null;
239
484
  }
485
+ const cache = readJson(CLAUDE_USAGE_CACHE_PATH, null);
486
+ const consecutive429s = Number.isFinite(cache?.consecutive429s)
487
+ ? cache.consecutive429s
488
+ : 0;
240
489
  let creds = readClaudeCredentials();
241
490
  if (!creds) {
242
491
  writeClaudeUsageCache(null, { type: "auth", status: 0 });
243
492
  return existingSnapshot.data || null;
244
493
  }
494
+ if (creds.supportsUsageApi === false) {
495
+ writeClaudeUsageCache(null, { type: "auth", status: 0 });
496
+ return existingSnapshot.data || null;
497
+ }
245
498
 
246
499
  // 토큰 만료 시 리프레시
247
500
  if (creds.expiresAt && creds.expiresAt <= Date.now() && creds.refreshToken) {
@@ -258,15 +511,37 @@ export async function fetchClaudeUsage(forceRefresh = false) {
258
511
  const result = await fetchClaudeUsageFromApi(creds.accessToken);
259
512
  if (!result.ok) {
260
513
  // 에러 유형별 분류하여 backoff 차등 적용
261
- const errorType = result.status === 429 ? "rate_limit"
262
- : result.status === 401 || result.status === 403 ? "auth"
263
- : result.error === "timeout" || result.error === "network" ? "network"
264
- : "unknown";
265
- writeClaudeUsageCache(existingSnapshot.data, { type: errorType, status: result.status });
514
+ const errorType =
515
+ result.status === 429
516
+ ? "rate_limit"
517
+ : result.status === 401 || result.status === 403
518
+ ? "auth"
519
+ : result.error === "timeout" || result.error === "network"
520
+ ? "network"
521
+ : "unknown";
522
+ const pollState =
523
+ errorType === "rate_limit"
524
+ ? computeClaudeUsagePollState({
525
+ consecutive429s,
526
+ outcome: "rate_limit",
527
+ })
528
+ : null;
529
+ writeClaudeUsageCache(
530
+ existingSnapshot.data,
531
+ { type: errorType, status: result.status },
532
+ pollState,
533
+ );
266
534
  return existingSnapshot.data || null;
267
535
  }
268
536
  const usage = parseClaudeUsageResponse(result.data);
269
- writeClaudeUsageCache(usage, usage ? null : { type: "unknown", status: 0 });
537
+ const pollState = usage
538
+ ? computeClaudeUsagePollState({ outcome: "success" })
539
+ : null;
540
+ writeClaudeUsageCache(
541
+ usage,
542
+ usage ? null : { type: "unknown", status: 0 },
543
+ pollState,
544
+ );
270
545
  return usage;
271
546
  }
272
547
 
@@ -278,23 +553,28 @@ export function scheduleClaudeUsageRefresh() {
278
553
  try {
279
554
  const omcCache = readJson(OMC_PLUGIN_USAGE_CACHE_PATH, null);
280
555
  if (omcCache?.data?.fiveHourPercent != null) {
281
- const omcAge = Number.isFinite(omcCache.timestamp) ? Date.now() - omcCache.timestamp : Infinity;
556
+ const omcAge = Number.isFinite(omcCache.timestamp)
557
+ ? Date.now() - omcCache.timestamp
558
+ : Infinity;
282
559
  if (omcAge < getClaudeUsageStaleMs()) {
283
560
  writeClaudeUsageCache(omcCache.data); // HUD 캐시에 복사만
284
561
  return;
285
562
  }
286
563
  }
287
- } catch { /* 무시 */ }
564
+ } catch {
565
+ /* 무시 */
566
+ }
288
567
 
289
568
  // 스폰 락: 30초 내 이미 스폰했으면 중복 방지 (첫 설치 시 429 방지)
290
- const lockPath = join(homedir(), ".claude", "cache", ".claude-refresh-lock");
291
569
  try {
292
- if (existsSync(lockPath)) {
293
- const lockAge = Date.now() - readJson(lockPath, {}).t;
294
- if (lockAge < 30000) return; // 30초 이내 스폰 이력 → 건너뜀
570
+ if (existsSync(CLAUDE_REFRESH_LOCK_PATH)) {
571
+ const lockAge = Date.now() - readJson(CLAUDE_REFRESH_LOCK_PATH, {}).t;
572
+ if (lockAge < SPAWN_LOCK_TTL_MS) return;
295
573
  }
296
- writeJsonSafe(lockPath, { t: Date.now() });
297
- } catch { /* 락 실패 무시 — 스폰 진행 */ }
574
+ writeJsonSafe(CLAUDE_REFRESH_LOCK_PATH, { t: Date.now() });
575
+ } catch {
576
+ /* 락 실패 무시 — 스폰 진행 */
577
+ }
298
578
 
299
579
  try {
300
580
  const child = spawn(process.execPath, [scriptPath, CLAUDE_REFRESH_FLAG], {
@@ -305,7 +585,11 @@ export function scheduleClaudeUsageRefresh() {
305
585
  child.unref();
306
586
  } catch (spawnErr) {
307
587
  // spawn 실패 시 에러 유형을 캐시에 기록 (HUD에서 원인 힌트 표시 가능)
308
- writeClaudeUsageCache(null, { type: "network", status: 0, hint: String(spawnErr?.message || spawnErr) });
588
+ writeClaudeUsageCache(null, {
589
+ type: "network",
590
+ status: 0,
591
+ hint: String(spawnErr?.message || spawnErr),
592
+ });
309
593
  }
310
594
  }
311
595