@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.
- package/hooks/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +183 -23
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +254 -33
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +831 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +77 -95
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +22 -16
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +184 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +153 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +184 -51
- package/hud/renderers.mjs +413 -123
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +48 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +70 -10
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
package/hud/providers/claude.mjs
CHANGED
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
2
|
// Claude Usage API (api.anthropic.com/api/oauth/usage)
|
|
3
3
|
// ============================================================================
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
27
|
-
|
|
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 =
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
197
|
+
}
|
|
198
|
+
resolve(null);
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
);
|
|
77
202
|
req.on("error", () => resolve(null));
|
|
78
|
-
req.on("timeout", () => {
|
|
203
|
+
req.on("timeout", () => {
|
|
204
|
+
req.destroy();
|
|
205
|
+
resolve(null);
|
|
206
|
+
});
|
|
79
207
|
req.end(body);
|
|
80
208
|
});
|
|
81
209
|
}
|
|
82
210
|
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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", () => {
|
|
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
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
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:
|
|
133
|
-
|
|
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))
|
|
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))
|
|
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
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
:
|
|
166
|
-
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
195
|
-
|
|
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)
|
|
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 {
|
|
421
|
+
return {
|
|
422
|
+
data: omcCache.data,
|
|
423
|
+
shouldRefresh: omcAge > getClaudeUsageStaleMs(),
|
|
424
|
+
isStale: false,
|
|
425
|
+
};
|
|
207
426
|
}
|
|
208
427
|
// stale이어도 data: null보다는 오래된 데이터를 fallback으로 표시
|
|
209
|
-
return {
|
|
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(
|
|
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,
|
|
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
|
|
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 =
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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)
|
|
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(
|
|
293
|
-
const lockAge = Date.now() - readJson(
|
|
294
|
-
if (lockAge <
|
|
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(
|
|
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, {
|
|
588
|
+
writeClaudeUsageCache(null, {
|
|
589
|
+
type: "network",
|
|
590
|
+
status: 0,
|
|
591
|
+
hint: String(spawnErr?.message || spawnErr),
|
|
592
|
+
});
|
|
309
593
|
}
|
|
310
594
|
}
|
|
311
595
|
|