@triflux/core 10.0.3 → 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 +47 -7
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +279 -33
- package/hooks/hook-registry.json +104 -7
- 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 +308 -18
- 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 +832 -291
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -3
- 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 -17
- package/hub/gemini-adapter.mjs +77 -95
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +171 -39
- 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 +303 -107
- 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 +116 -58
- package/hub/research.mjs +34 -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 +129 -63
- package/hub/state.mjs +86 -44
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/token-mode.mjs +76 -72
- 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 +393 -249
- 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 +1282 -116
- 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 +72 -12
- 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 +36 -11
- 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/codex.mjs
CHANGED
|
@@ -1,21 +1,52 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
2
|
// Codex rate limits 추출 / 캐싱
|
|
3
3
|
// ============================================================================
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
5
7
|
import { homedir } from "node:os";
|
|
6
8
|
import { join } from "node:path";
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
9
|
import {
|
|
9
|
-
CODEX_AUTH_PATH,
|
|
10
|
-
CODEX_MIN_BUCKETS,
|
|
10
|
+
CODEX_AUTH_PATH,
|
|
11
|
+
CODEX_MIN_BUCKETS,
|
|
12
|
+
CODEX_QUOTA_CACHE_PATH,
|
|
13
|
+
CODEX_QUOTA_STALE_MS,
|
|
14
|
+
CODEX_REFRESH_FLAG,
|
|
15
|
+
CODEX_REFRESH_LOCK_PATH,
|
|
16
|
+
SPAWN_LOCK_TTL_MS,
|
|
11
17
|
} from "../constants.mjs";
|
|
12
|
-
import { readJson, writeJsonSafe
|
|
18
|
+
import { decodeJwtEmail, readJson, writeJsonSafe } from "../utils.mjs";
|
|
19
|
+
|
|
20
|
+
// window_minutes 기반 5h/1w 슬롯 분류
|
|
21
|
+
// 5h: ≤360min (300min 표준, ±20% 허용)
|
|
22
|
+
// weekly: ≥7000min (10080min 표준, ~5d 이상만 허용 — 24h/48h 등 중간 버킷 도입 시 silent 오분류 방지)
|
|
23
|
+
export function classifyBucket(bucket) {
|
|
24
|
+
if (!bucket?.window_minutes) return null;
|
|
25
|
+
if (bucket.window_minutes <= 360) return "five_hour";
|
|
26
|
+
if (bucket.window_minutes >= 7000) return "weekly";
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// primary/secondary를 window_minutes 기준으로 정규화
|
|
31
|
+
// HUD 규약: primary=5h, secondary=1w
|
|
32
|
+
export function normalizeBuckets(rl) {
|
|
33
|
+
let primary = null;
|
|
34
|
+
let secondary = null;
|
|
35
|
+
for (const bucket of [rl.primary, rl.secondary].filter(Boolean)) {
|
|
36
|
+
const kind = classifyBucket(bucket);
|
|
37
|
+
if (kind === "five_hour") primary = bucket;
|
|
38
|
+
else if (kind === "weekly") secondary = bucket;
|
|
39
|
+
}
|
|
40
|
+
return { primary, secondary };
|
|
41
|
+
}
|
|
13
42
|
|
|
14
43
|
export function getCodexEmail() {
|
|
15
44
|
try {
|
|
16
45
|
const auth = JSON.parse(readFileSync(CODEX_AUTH_PATH, "utf-8"));
|
|
17
46
|
return decodeJwtEmail(auth?.tokens?.id_token);
|
|
18
|
-
} catch {
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
19
50
|
}
|
|
20
51
|
|
|
21
52
|
// resets_at이 지난 윈도우의 used_percent를 0으로 보정
|
|
@@ -24,13 +55,22 @@ export function expireStaleCodexBuckets(buckets) {
|
|
|
24
55
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
25
56
|
const result = {};
|
|
26
57
|
for (const [key, bucket] of Object.entries(buckets)) {
|
|
27
|
-
if (!bucket) {
|
|
58
|
+
if (!bucket) {
|
|
59
|
+
result[key] = bucket;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
28
62
|
let updated = bucket;
|
|
29
63
|
if (bucket.primary?.resets_at && bucket.primary.resets_at <= nowSec) {
|
|
30
|
-
updated = {
|
|
64
|
+
updated = {
|
|
65
|
+
...updated,
|
|
66
|
+
primary: { ...updated.primary, used_percent: 0 },
|
|
67
|
+
};
|
|
31
68
|
}
|
|
32
69
|
if (bucket.secondary?.resets_at && bucket.secondary.resets_at <= nowSec) {
|
|
33
|
-
updated = {
|
|
70
|
+
updated = {
|
|
71
|
+
...updated,
|
|
72
|
+
secondary: { ...updated.secondary, used_percent: 0 },
|
|
73
|
+
};
|
|
34
74
|
}
|
|
35
75
|
result[key] = updated;
|
|
36
76
|
}
|
|
@@ -51,15 +91,23 @@ export function getCodexRateLimits() {
|
|
|
51
91
|
for (let dayOffset = 0; dayOffset <= 6; dayOffset++) {
|
|
52
92
|
const d = new Date(now.getTime() - dayOffset * 86_400_000);
|
|
53
93
|
const sessDir = join(
|
|
54
|
-
homedir(),
|
|
94
|
+
homedir(),
|
|
95
|
+
".codex",
|
|
96
|
+
"sessions",
|
|
55
97
|
String(d.getFullYear()),
|
|
56
98
|
String(d.getMonth() + 1).padStart(2, "0"),
|
|
57
99
|
String(d.getDate()).padStart(2, "0"),
|
|
58
100
|
);
|
|
59
101
|
if (!existsSync(sessDir)) continue;
|
|
60
102
|
let files;
|
|
61
|
-
try {
|
|
62
|
-
|
|
103
|
+
try {
|
|
104
|
+
files = readdirSync(sessDir)
|
|
105
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
106
|
+
.sort()
|
|
107
|
+
.reverse();
|
|
108
|
+
} catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
63
111
|
|
|
64
112
|
const mergedBuckets = {};
|
|
65
113
|
for (const file of files) {
|
|
@@ -71,35 +119,50 @@ export function getCodexRateLimits() {
|
|
|
71
119
|
const evt = JSON.parse(line);
|
|
72
120
|
const rl = evt?.payload?.rate_limits;
|
|
73
121
|
if (rl?.limit_id && !mergedBuckets[rl.limit_id]) {
|
|
74
|
-
//
|
|
122
|
+
// window_minutes 기준으로 5h/1w 슬롯 정규화
|
|
123
|
+
const { primary, secondary } = normalizeBuckets(rl);
|
|
75
124
|
mergedBuckets[rl.limit_id] = {
|
|
76
|
-
limitId: rl.limit_id,
|
|
77
|
-
|
|
125
|
+
limitId: rl.limit_id,
|
|
126
|
+
limitName: rl.limit_name,
|
|
127
|
+
primary,
|
|
128
|
+
secondary,
|
|
78
129
|
credits: rl.credits,
|
|
79
130
|
tokens: evt.payload?.info?.total_token_usage,
|
|
80
131
|
contextWindow: evt.payload?.info?.model_context_window,
|
|
81
132
|
timestamp: evt.timestamp,
|
|
82
133
|
};
|
|
83
|
-
} else if (
|
|
134
|
+
} else if (
|
|
135
|
+
dayOffset <= 1 &&
|
|
136
|
+
!rl &&
|
|
137
|
+
evt?.payload?.info?.total_token_usage &&
|
|
138
|
+
!syntheticBucket
|
|
139
|
+
) {
|
|
84
140
|
// 2일 이내 token_count: 합성 버킷 (rate_limits가 null일 때 행 활성화용, stale 방지)
|
|
85
141
|
syntheticBucket = {
|
|
86
|
-
limitId: "codex",
|
|
87
|
-
|
|
142
|
+
limitId: "codex",
|
|
143
|
+
limitName: "codex-session",
|
|
144
|
+
primary: null,
|
|
145
|
+
secondary: null,
|
|
88
146
|
credits: null,
|
|
89
147
|
tokens: evt.payload.info.total_token_usage,
|
|
90
148
|
contextWindow: evt.payload.info.model_context_window,
|
|
91
149
|
timestamp: evt.timestamp,
|
|
92
150
|
};
|
|
93
151
|
}
|
|
94
|
-
} catch {
|
|
152
|
+
} catch {
|
|
153
|
+
/* 라인 파싱 실패 무시 */
|
|
154
|
+
}
|
|
95
155
|
if (Object.keys(mergedBuckets).length >= CODEX_MIN_BUCKETS) break;
|
|
96
156
|
}
|
|
97
|
-
} catch {
|
|
157
|
+
} catch {
|
|
158
|
+
/* 파일 읽기 실패 무시 */
|
|
159
|
+
}
|
|
98
160
|
}
|
|
99
161
|
// 실제 rate_limits 발견 → 토큰 데이터 병합 후 즉시 반환
|
|
100
162
|
if (Object.keys(mergedBuckets).length > 0) {
|
|
101
163
|
if (syntheticBucket) {
|
|
102
|
-
const main =
|
|
164
|
+
const main =
|
|
165
|
+
mergedBuckets.codex || mergedBuckets[Object.keys(mergedBuckets)[0]];
|
|
103
166
|
if (main && !main.tokens) main.tokens = syntheticBucket.tokens;
|
|
104
167
|
}
|
|
105
168
|
expireStaleCodexBuckets(mergedBuckets);
|
|
@@ -132,6 +195,18 @@ export function refreshCodexRateLimitsCache() {
|
|
|
132
195
|
export function scheduleCodexRateLimitRefresh() {
|
|
133
196
|
const scriptPath = process.argv[1];
|
|
134
197
|
if (!scriptPath) return;
|
|
198
|
+
|
|
199
|
+
// 스폰 락: 30초 내 이미 스폰했으면 중복 방지
|
|
200
|
+
try {
|
|
201
|
+
if (existsSync(CODEX_REFRESH_LOCK_PATH)) {
|
|
202
|
+
const lockAge = Date.now() - readJson(CODEX_REFRESH_LOCK_PATH, {}).t;
|
|
203
|
+
if (lockAge < SPAWN_LOCK_TTL_MS) return;
|
|
204
|
+
}
|
|
205
|
+
writeJsonSafe(CODEX_REFRESH_LOCK_PATH, { t: Date.now() });
|
|
206
|
+
} catch {
|
|
207
|
+
/* 락 실패 무시 — 스폰 진행 */
|
|
208
|
+
}
|
|
209
|
+
|
|
135
210
|
try {
|
|
136
211
|
const child = spawn(process.execPath, [scriptPath, CODEX_REFRESH_FLAG], {
|
|
137
212
|
detached: true,
|
package/hud/providers/gemini.mjs
CHANGED
|
@@ -1,30 +1,47 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
2
|
// Gemini 쿼터 API / 세션 토큰 / RPM 트래커
|
|
3
3
|
// ============================================================================
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import https from "node:https";
|
|
5
8
|
import { homedir } from "node:os";
|
|
6
9
|
import { join } from "node:path";
|
|
7
|
-
import https from "node:https";
|
|
8
|
-
import { spawn } from "node:child_process";
|
|
9
10
|
import {
|
|
10
|
-
GEMINI_OAUTH_PATH, GEMINI_QUOTA_CACHE_PATH, GEMINI_PROJECT_CACHE_PATH,
|
|
11
|
-
GEMINI_SESSION_CACHE_PATH, GEMINI_RPM_TRACKER_PATH,
|
|
12
|
-
LEGACY_GEMINI_QUOTA_CACHE, LEGACY_GEMINI_PROJECT_CACHE,
|
|
13
|
-
LEGACY_GEMINI_SESSION_CACHE, LEGACY_GEMINI_RPM_TRACKER,
|
|
14
|
-
GEMINI_RPM_WINDOW_MS, GEMINI_QUOTA_STALE_MS, GEMINI_SESSION_STALE_MS,
|
|
15
11
|
GEMINI_API_TIMEOUT_MS,
|
|
16
|
-
|
|
12
|
+
GEMINI_OAUTH_PATH,
|
|
13
|
+
GEMINI_PROJECT_CACHE_PATH,
|
|
14
|
+
GEMINI_QUOTA_CACHE_PATH,
|
|
15
|
+
GEMINI_QUOTA_REFRESH_LOCK_PATH,
|
|
16
|
+
GEMINI_QUOTA_STALE_MS,
|
|
17
|
+
GEMINI_REFRESH_FLAG,
|
|
18
|
+
GEMINI_RPM_TRACKER_PATH,
|
|
19
|
+
GEMINI_RPM_WINDOW_MS,
|
|
20
|
+
GEMINI_SESSION_CACHE_PATH,
|
|
21
|
+
GEMINI_SESSION_REFRESH_FLAG,
|
|
22
|
+
GEMINI_SESSION_REFRESH_LOCK_PATH,
|
|
23
|
+
GEMINI_SESSION_STALE_MS,
|
|
24
|
+
LEGACY_GEMINI_PROJECT_CACHE,
|
|
25
|
+
LEGACY_GEMINI_QUOTA_CACHE,
|
|
26
|
+
LEGACY_GEMINI_RPM_TRACKER,
|
|
27
|
+
LEGACY_GEMINI_SESSION_CACHE,
|
|
28
|
+
SPAWN_LOCK_TTL_MS,
|
|
17
29
|
} from "../constants.mjs";
|
|
18
30
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
31
|
+
clampPercent,
|
|
32
|
+
createHttpsPost,
|
|
33
|
+
decodeJwtEmail,
|
|
34
|
+
makeHash,
|
|
35
|
+
readJson,
|
|
36
|
+
readJsonMigrate,
|
|
37
|
+
writeJsonSafe,
|
|
21
38
|
} from "../utils.mjs";
|
|
22
39
|
|
|
23
40
|
const httpsPost = createHttpsPost(https, GEMINI_API_TIMEOUT_MS);
|
|
24
41
|
|
|
25
42
|
// Gemini 모델별 RPM 한도 (실측 기반: Pro 25, Flash 300)
|
|
26
43
|
export function getGeminiRpmLimit(model) {
|
|
27
|
-
if (model
|
|
44
|
+
if (model?.includes("pro")) return 25;
|
|
28
45
|
return 300; // Flash 기본
|
|
29
46
|
}
|
|
30
47
|
|
|
@@ -48,21 +65,36 @@ export function deriveGeminiLimits(bucket) {
|
|
|
48
65
|
if (bucket.remainingAmount != null) {
|
|
49
66
|
const remaining = parseInt(bucket.remainingAmount, 10);
|
|
50
67
|
const limit = fraction > 0 ? Math.round(remaining / fraction) : 0;
|
|
51
|
-
return {
|
|
68
|
+
return {
|
|
69
|
+
usedPct,
|
|
70
|
+
remaining,
|
|
71
|
+
limit,
|
|
72
|
+
resetTime: bucket.resetTime,
|
|
73
|
+
modelId: bucket.modelId,
|
|
74
|
+
};
|
|
52
75
|
}
|
|
53
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
usedPct,
|
|
78
|
+
remaining: null,
|
|
79
|
+
limit: null,
|
|
80
|
+
resetTime: bucket.resetTime,
|
|
81
|
+
modelId: bucket.modelId,
|
|
82
|
+
};
|
|
54
83
|
}
|
|
55
84
|
|
|
56
85
|
export function getGeminiEmail() {
|
|
57
86
|
try {
|
|
58
87
|
const oauth = readJson(GEMINI_OAUTH_PATH, null);
|
|
59
88
|
return decodeJwtEmail(oauth?.id_token);
|
|
60
|
-
} catch {
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
61
92
|
}
|
|
62
93
|
|
|
63
94
|
export function buildGeminiAuthContext(accountId) {
|
|
64
95
|
const oauth = readJson(GEMINI_OAUTH_PATH, null);
|
|
65
|
-
const tokenSource =
|
|
96
|
+
const tokenSource =
|
|
97
|
+
oauth?.refresh_token || oauth?.id_token || oauth?.access_token || "";
|
|
66
98
|
const tokenFingerprint = tokenSource ? makeHash(tokenSource) : "none";
|
|
67
99
|
const cacheKey = `${accountId || "gemini-main"}::${tokenFingerprint}`;
|
|
68
100
|
return { oauth, tokenFingerprint, cacheKey };
|
|
@@ -77,11 +109,17 @@ export async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
77
109
|
const forceRefresh = options.forceRefresh === true;
|
|
78
110
|
|
|
79
111
|
// 1. 캐시 확인 (계정/토큰별)
|
|
80
|
-
const cache = readJsonMigrate(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
112
|
+
const cache = readJsonMigrate(
|
|
113
|
+
GEMINI_QUOTA_CACHE_PATH,
|
|
114
|
+
LEGACY_GEMINI_QUOTA_CACHE,
|
|
115
|
+
null,
|
|
116
|
+
);
|
|
117
|
+
if (
|
|
118
|
+
!forceRefresh &&
|
|
119
|
+
cache?.cacheKey === cacheKey &&
|
|
120
|
+
cache?.timestamp &&
|
|
121
|
+
Date.now() - cache.timestamp < GEMINI_QUOTA_STALE_MS
|
|
122
|
+
) {
|
|
85
123
|
return cache;
|
|
86
124
|
}
|
|
87
125
|
|
|
@@ -116,12 +154,22 @@ export async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
116
154
|
oauth.access_token,
|
|
117
155
|
);
|
|
118
156
|
const id = loadRes?.cloudaicompanionProject;
|
|
119
|
-
if (id)
|
|
157
|
+
if (id)
|
|
158
|
+
writeJsonSafe(GEMINI_PROJECT_CACHE_PATH, {
|
|
159
|
+
cacheKey,
|
|
160
|
+
projectId: id,
|
|
161
|
+
timestamp: Date.now(),
|
|
162
|
+
});
|
|
120
163
|
return id || null;
|
|
121
164
|
};
|
|
122
165
|
|
|
123
|
-
const projCache = readJsonMigrate(
|
|
124
|
-
|
|
166
|
+
const projCache = readJsonMigrate(
|
|
167
|
+
GEMINI_PROJECT_CACHE_PATH,
|
|
168
|
+
LEGACY_GEMINI_PROJECT_CACHE,
|
|
169
|
+
null,
|
|
170
|
+
);
|
|
171
|
+
let projectId =
|
|
172
|
+
projCache?.cacheKey === cacheKey ? projCache?.projectId : null;
|
|
125
173
|
if (!projectId) projectId = await fetchProjectId();
|
|
126
174
|
if (!projectId) return cache;
|
|
127
175
|
|
|
@@ -145,7 +193,11 @@ export async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
145
193
|
|
|
146
194
|
if (!quotaRes?.buckets) {
|
|
147
195
|
// API 응답에 buckets 없음: 에러 코드 또는 응답 내용을 캐시에 기록
|
|
148
|
-
const apiError =
|
|
196
|
+
const apiError =
|
|
197
|
+
quotaRes?.error?.message ||
|
|
198
|
+
quotaRes?.error?.code ||
|
|
199
|
+
quotaRes?.error ||
|
|
200
|
+
"no buckets in response";
|
|
149
201
|
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
150
202
|
...(cache || {}),
|
|
151
203
|
timestamp: cache?.timestamp || Date.now(),
|
|
@@ -175,12 +227,17 @@ export async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
175
227
|
export function readGeminiRpm(model) {
|
|
176
228
|
try {
|
|
177
229
|
// 새 경로 → 레거시 경로 fallback
|
|
178
|
-
const rpmPath = existsSync(GEMINI_RPM_TRACKER_PATH)
|
|
179
|
-
|
|
230
|
+
const rpmPath = existsSync(GEMINI_RPM_TRACKER_PATH)
|
|
231
|
+
? GEMINI_RPM_TRACKER_PATH
|
|
232
|
+
: existsSync(LEGACY_GEMINI_RPM_TRACKER)
|
|
233
|
+
? LEGACY_GEMINI_RPM_TRACKER
|
|
234
|
+
: null;
|
|
180
235
|
if (!rpmPath) return { count: 0, percent: 0, remainingSec: 60 };
|
|
181
236
|
const raw = readFileSync(rpmPath, "utf-8");
|
|
182
237
|
const parsed = JSON.parse(raw);
|
|
183
|
-
const timestamps = Array.isArray(parsed.timestamps)
|
|
238
|
+
const timestamps = Array.isArray(parsed.timestamps)
|
|
239
|
+
? parsed.timestamps
|
|
240
|
+
: [];
|
|
184
241
|
const now = Date.now();
|
|
185
242
|
const recent = timestamps.filter((t) => now - t < GEMINI_RPM_WINDOW_MS);
|
|
186
243
|
const count = recent.length;
|
|
@@ -188,9 +245,15 @@ export function readGeminiRpm(model) {
|
|
|
188
245
|
const percent = clampPercent(Math.round((count / rpmLimit) * 100));
|
|
189
246
|
// 가장 오래된 엔트리가 윈도우에서 빠지기까지 남은 초 (0건이면 0s)
|
|
190
247
|
// 5초 단위 반올림으로 HUD 깜빡임 감소
|
|
191
|
-
const rawRemainingSec =
|
|
192
|
-
|
|
193
|
-
|
|
248
|
+
const rawRemainingSec =
|
|
249
|
+
recent.length > 0
|
|
250
|
+
? Math.max(
|
|
251
|
+
0,
|
|
252
|
+
Math.ceil(
|
|
253
|
+
(GEMINI_RPM_WINDOW_MS - (now - Math.min(...recent))) / 1000,
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
: 0;
|
|
194
257
|
const remainingSec = Math.ceil(rawRemainingSec / 5) * 5;
|
|
195
258
|
return { count, percent, remainingSec };
|
|
196
259
|
} catch {
|
|
@@ -199,7 +262,11 @@ export function readGeminiRpm(model) {
|
|
|
199
262
|
}
|
|
200
263
|
|
|
201
264
|
export function readGeminiQuotaSnapshot(accountId, authContext) {
|
|
202
|
-
const cache = readJsonMigrate(
|
|
265
|
+
const cache = readJsonMigrate(
|
|
266
|
+
GEMINI_QUOTA_CACHE_PATH,
|
|
267
|
+
LEGACY_GEMINI_QUOTA_CACHE,
|
|
268
|
+
null,
|
|
269
|
+
);
|
|
203
270
|
if (!cache?.buckets) {
|
|
204
271
|
return { quota: null, shouldRefresh: true };
|
|
205
272
|
}
|
|
@@ -208,19 +275,24 @@ export function readGeminiQuotaSnapshot(accountId, authContext) {
|
|
|
208
275
|
const isLegacyCache = !cache.cacheKey;
|
|
209
276
|
const keyMatched = cache.cacheKey === cacheKey;
|
|
210
277
|
const cacheTs = Number(cache.timestamp);
|
|
211
|
-
const ageMs = Number.isFinite(cacheTs)
|
|
278
|
+
const ageMs = Number.isFinite(cacheTs)
|
|
279
|
+
? Date.now() - cacheTs
|
|
280
|
+
: Number.MAX_SAFE_INTEGER;
|
|
212
281
|
const isFresh = ageMs < GEMINI_QUOTA_STALE_MS;
|
|
213
282
|
|
|
214
283
|
if (keyMatched) {
|
|
215
284
|
// resetTime이 지난 버킷의 remainingFraction을 1로 보정 (stale 캐시 방지)
|
|
216
285
|
if (Array.isArray(cache.buckets)) {
|
|
217
286
|
const now = Date.now();
|
|
218
|
-
const patchedBuckets = cache.buckets.map(b =>
|
|
287
|
+
const patchedBuckets = cache.buckets.map((b) =>
|
|
219
288
|
b?.resetTime && new Date(b.resetTime).getTime() <= now
|
|
220
289
|
? { ...b, remainingFraction: 1 }
|
|
221
|
-
: b
|
|
290
|
+
: b,
|
|
222
291
|
);
|
|
223
|
-
return {
|
|
292
|
+
return {
|
|
293
|
+
quota: { ...cache, buckets: patchedBuckets },
|
|
294
|
+
shouldRefresh: !isFresh,
|
|
295
|
+
};
|
|
224
296
|
}
|
|
225
297
|
return { quota: cache, shouldRefresh: !isFresh };
|
|
226
298
|
}
|
|
@@ -233,10 +305,28 @@ export function readGeminiQuotaSnapshot(accountId, authContext) {
|
|
|
233
305
|
export function scheduleGeminiQuotaRefresh(accountId) {
|
|
234
306
|
const scriptPath = process.argv[1];
|
|
235
307
|
if (!scriptPath) return;
|
|
308
|
+
|
|
309
|
+
// 스폰 락: 30초 내 이미 스폰했으면 중복 방지
|
|
310
|
+
try {
|
|
311
|
+
if (existsSync(GEMINI_QUOTA_REFRESH_LOCK_PATH)) {
|
|
312
|
+
const lockAge =
|
|
313
|
+
Date.now() - readJson(GEMINI_QUOTA_REFRESH_LOCK_PATH, {}).t;
|
|
314
|
+
if (lockAge < SPAWN_LOCK_TTL_MS) return;
|
|
315
|
+
}
|
|
316
|
+
writeJsonSafe(GEMINI_QUOTA_REFRESH_LOCK_PATH, { t: Date.now() });
|
|
317
|
+
} catch {
|
|
318
|
+
/* 락 실패 무시 — 스폰 진행 */
|
|
319
|
+
}
|
|
320
|
+
|
|
236
321
|
try {
|
|
237
322
|
const child = spawn(
|
|
238
323
|
process.execPath,
|
|
239
|
-
[
|
|
324
|
+
[
|
|
325
|
+
scriptPath,
|
|
326
|
+
GEMINI_REFRESH_FLAG,
|
|
327
|
+
"--account",
|
|
328
|
+
accountId || "gemini-main",
|
|
329
|
+
],
|
|
240
330
|
{
|
|
241
331
|
detached: true,
|
|
242
332
|
stdio: "ignore",
|
|
@@ -255,7 +345,11 @@ export function scheduleGeminiQuotaRefresh(accountId) {
|
|
|
255
345
|
}
|
|
256
346
|
|
|
257
347
|
export function readGeminiSessionSnapshot() {
|
|
258
|
-
const cache = readJsonMigrate(
|
|
348
|
+
const cache = readJsonMigrate(
|
|
349
|
+
GEMINI_SESSION_CACHE_PATH,
|
|
350
|
+
LEGACY_GEMINI_SESSION_CACHE,
|
|
351
|
+
null,
|
|
352
|
+
);
|
|
259
353
|
if (!cache?.session) {
|
|
260
354
|
return { session: null, shouldRefresh: true };
|
|
261
355
|
}
|
|
@@ -275,14 +369,33 @@ export function refreshGeminiSessionCache() {
|
|
|
275
369
|
export function scheduleGeminiSessionRefresh() {
|
|
276
370
|
const scriptPath = process.argv[1];
|
|
277
371
|
if (!scriptPath) return;
|
|
372
|
+
|
|
373
|
+
// 스폰 락: 30초 내 이미 스폰했으면 중복 방지
|
|
278
374
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
375
|
+
if (existsSync(GEMINI_SESSION_REFRESH_LOCK_PATH)) {
|
|
376
|
+
const lockAge =
|
|
377
|
+
Date.now() - readJson(GEMINI_SESSION_REFRESH_LOCK_PATH, {}).t;
|
|
378
|
+
if (lockAge < SPAWN_LOCK_TTL_MS) return;
|
|
379
|
+
}
|
|
380
|
+
writeJsonSafe(GEMINI_SESSION_REFRESH_LOCK_PATH, { t: Date.now() });
|
|
381
|
+
} catch {
|
|
382
|
+
/* 락 실패 무시 — 스폰 진행 */
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
const child = spawn(
|
|
387
|
+
process.execPath,
|
|
388
|
+
[scriptPath, GEMINI_SESSION_REFRESH_FLAG],
|
|
389
|
+
{
|
|
390
|
+
detached: true,
|
|
391
|
+
stdio: "ignore",
|
|
392
|
+
windowsHide: true,
|
|
393
|
+
},
|
|
394
|
+
);
|
|
284
395
|
child.unref();
|
|
285
|
-
} catch {
|
|
396
|
+
} catch {
|
|
397
|
+
/* 백그라운드 실행 실패 무시 */
|
|
398
|
+
}
|
|
286
399
|
}
|
|
287
400
|
|
|
288
401
|
// ============================================================================
|
|
@@ -294,27 +407,47 @@ export function scanGeminiSessionTokens() {
|
|
|
294
407
|
let best = null;
|
|
295
408
|
let bestTime = 0;
|
|
296
409
|
try {
|
|
297
|
-
const dirs = readdirSync(tmpDir).filter((d) =>
|
|
410
|
+
const dirs = readdirSync(tmpDir).filter((d) =>
|
|
411
|
+
existsSync(join(tmpDir, d, "chats")),
|
|
412
|
+
);
|
|
298
413
|
for (const dir of dirs) {
|
|
299
414
|
const chatsDir = join(tmpDir, dir, "chats");
|
|
300
415
|
let files;
|
|
301
|
-
try {
|
|
416
|
+
try {
|
|
417
|
+
files = readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
|
|
418
|
+
} catch {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
302
421
|
for (const file of files) {
|
|
303
422
|
try {
|
|
304
423
|
const data = JSON.parse(readFileSync(join(chatsDir, file), "utf-8"));
|
|
305
424
|
const updatedAt = new Date(data.lastUpdated || 0).getTime();
|
|
306
425
|
if (updatedAt <= bestTime) continue;
|
|
307
|
-
let input = 0,
|
|
426
|
+
let input = 0,
|
|
427
|
+
output = 0;
|
|
308
428
|
let model = "unknown";
|
|
309
429
|
for (const msg of data.messages || []) {
|
|
310
|
-
if (msg.tokens) {
|
|
430
|
+
if (msg.tokens) {
|
|
431
|
+
input += msg.tokens.input || 0;
|
|
432
|
+
output += msg.tokens.output || 0;
|
|
433
|
+
}
|
|
311
434
|
if (msg.model) model = msg.model;
|
|
312
435
|
}
|
|
313
436
|
bestTime = updatedAt;
|
|
314
|
-
best = {
|
|
315
|
-
|
|
437
|
+
best = {
|
|
438
|
+
input,
|
|
439
|
+
output,
|
|
440
|
+
total: input + output,
|
|
441
|
+
model,
|
|
442
|
+
lastUpdated: data.lastUpdated,
|
|
443
|
+
};
|
|
444
|
+
} catch {
|
|
445
|
+
/* 무시 */
|
|
446
|
+
}
|
|
316
447
|
}
|
|
317
448
|
}
|
|
318
|
-
} catch {
|
|
449
|
+
} catch {
|
|
450
|
+
/* 무시 */
|
|
451
|
+
}
|
|
319
452
|
return best;
|
|
320
453
|
}
|