@triflux/core 10.1.0 → 10.26.0
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 +191 -31
- 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 +262 -34
- 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 +1051 -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 +22 -21
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/schema/delegator-tools.schema.json +1 -1
- package/hub/delegator/service.mjs +93 -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 +82 -108
- 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 +78 -30
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/claude-daemon-control.mjs +1130 -0
- package/hub/team/retry-state-machine.mjs +336 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +23 -17
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +190 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +157 -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 +210 -52
- package/hud/renderers.mjs +440 -127
- 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 +37 -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 +49 -20
- 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 +117 -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 +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/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,48 @@
|
|
|
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
|
-
|
|
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,
|
|
11
|
+
ANTIGRAVITY_OAUTH_PATHS,
|
|
15
12
|
GEMINI_API_TIMEOUT_MS,
|
|
16
|
-
|
|
13
|
+
GEMINI_OAUTH_PATH,
|
|
14
|
+
GEMINI_PROJECT_CACHE_PATH,
|
|
15
|
+
GEMINI_QUOTA_CACHE_PATH,
|
|
16
|
+
GEMINI_QUOTA_REFRESH_LOCK_PATH,
|
|
17
|
+
GEMINI_QUOTA_STALE_MS,
|
|
18
|
+
GEMINI_REFRESH_FLAG,
|
|
19
|
+
GEMINI_RPM_TRACKER_PATH,
|
|
20
|
+
GEMINI_RPM_WINDOW_MS,
|
|
21
|
+
GEMINI_SESSION_CACHE_PATH,
|
|
22
|
+
GEMINI_SESSION_REFRESH_FLAG,
|
|
23
|
+
GEMINI_SESSION_REFRESH_LOCK_PATH,
|
|
24
|
+
GEMINI_SESSION_STALE_MS,
|
|
25
|
+
LEGACY_GEMINI_PROJECT_CACHE,
|
|
26
|
+
LEGACY_GEMINI_QUOTA_CACHE,
|
|
27
|
+
LEGACY_GEMINI_RPM_TRACKER,
|
|
28
|
+
LEGACY_GEMINI_SESSION_CACHE,
|
|
29
|
+
SPAWN_LOCK_TTL_MS,
|
|
17
30
|
} from "../constants.mjs";
|
|
18
31
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
clampPercent,
|
|
33
|
+
createHttpsPost,
|
|
34
|
+
decodeJwtEmail,
|
|
35
|
+
makeHash,
|
|
36
|
+
readJson,
|
|
37
|
+
readJsonMigrate,
|
|
38
|
+
writeJsonSafe,
|
|
21
39
|
} from "../utils.mjs";
|
|
22
40
|
|
|
23
41
|
const httpsPost = createHttpsPost(https, GEMINI_API_TIMEOUT_MS);
|
|
24
42
|
|
|
25
43
|
// Gemini 모델별 RPM 한도 (실측 기반: Pro 25, Flash 300)
|
|
26
44
|
export function getGeminiRpmLimit(model) {
|
|
27
|
-
if (model
|
|
45
|
+
if (model?.includes("pro")) return 25;
|
|
28
46
|
return 300; // Flash 기본
|
|
29
47
|
}
|
|
30
48
|
|
|
@@ -48,21 +66,60 @@ export function deriveGeminiLimits(bucket) {
|
|
|
48
66
|
if (bucket.remainingAmount != null) {
|
|
49
67
|
const remaining = parseInt(bucket.remainingAmount, 10);
|
|
50
68
|
const limit = fraction > 0 ? Math.round(remaining / fraction) : 0;
|
|
51
|
-
return {
|
|
69
|
+
return {
|
|
70
|
+
usedPct,
|
|
71
|
+
remaining,
|
|
72
|
+
limit,
|
|
73
|
+
resetTime: bucket.resetTime,
|
|
74
|
+
modelId: bucket.modelId,
|
|
75
|
+
};
|
|
52
76
|
}
|
|
53
|
-
return {
|
|
77
|
+
return {
|
|
78
|
+
usedPct,
|
|
79
|
+
remaining: null,
|
|
80
|
+
limit: null,
|
|
81
|
+
resetTime: bucket.resetTime,
|
|
82
|
+
modelId: bucket.modelId,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getCredentialEmail(credential) {
|
|
87
|
+
return (
|
|
88
|
+
decodeJwtEmail(credential?.id_token) ||
|
|
89
|
+
credential?.email ||
|
|
90
|
+
credential?.account?.email ||
|
|
91
|
+
credential?.user?.email ||
|
|
92
|
+
credential?.profile?.email ||
|
|
93
|
+
null
|
|
94
|
+
);
|
|
54
95
|
}
|
|
55
96
|
|
|
56
97
|
export function getGeminiEmail() {
|
|
57
98
|
try {
|
|
58
99
|
const oauth = readJson(GEMINI_OAUTH_PATH, null);
|
|
59
|
-
return
|
|
60
|
-
} catch {
|
|
100
|
+
return getCredentialEmail(oauth);
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getAntigravityEmail() {
|
|
107
|
+
for (const oauthPath of ANTIGRAVITY_OAUTH_PATHS) {
|
|
108
|
+
try {
|
|
109
|
+
const oauth = readJson(oauthPath, null);
|
|
110
|
+
const email = getCredentialEmail(oauth);
|
|
111
|
+
if (email) return email;
|
|
112
|
+
} catch {
|
|
113
|
+
// Try the next known Antigravity credential path.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
61
117
|
}
|
|
62
118
|
|
|
63
119
|
export function buildGeminiAuthContext(accountId) {
|
|
64
120
|
const oauth = readJson(GEMINI_OAUTH_PATH, null);
|
|
65
|
-
const tokenSource =
|
|
121
|
+
const tokenSource =
|
|
122
|
+
oauth?.refresh_token || oauth?.id_token || oauth?.access_token || "";
|
|
66
123
|
const tokenFingerprint = tokenSource ? makeHash(tokenSource) : "none";
|
|
67
124
|
const cacheKey = `${accountId || "gemini-main"}::${tokenFingerprint}`;
|
|
68
125
|
return { oauth, tokenFingerprint, cacheKey };
|
|
@@ -77,11 +134,17 @@ export async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
77
134
|
const forceRefresh = options.forceRefresh === true;
|
|
78
135
|
|
|
79
136
|
// 1. 캐시 확인 (계정/토큰별)
|
|
80
|
-
const cache = readJsonMigrate(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
137
|
+
const cache = readJsonMigrate(
|
|
138
|
+
GEMINI_QUOTA_CACHE_PATH,
|
|
139
|
+
LEGACY_GEMINI_QUOTA_CACHE,
|
|
140
|
+
null,
|
|
141
|
+
);
|
|
142
|
+
if (
|
|
143
|
+
!forceRefresh &&
|
|
144
|
+
cache?.cacheKey === cacheKey &&
|
|
145
|
+
cache?.timestamp &&
|
|
146
|
+
Date.now() - cache.timestamp < GEMINI_QUOTA_STALE_MS
|
|
147
|
+
) {
|
|
85
148
|
return cache;
|
|
86
149
|
}
|
|
87
150
|
|
|
@@ -116,12 +179,22 @@ export async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
116
179
|
oauth.access_token,
|
|
117
180
|
);
|
|
118
181
|
const id = loadRes?.cloudaicompanionProject;
|
|
119
|
-
if (id)
|
|
182
|
+
if (id)
|
|
183
|
+
writeJsonSafe(GEMINI_PROJECT_CACHE_PATH, {
|
|
184
|
+
cacheKey,
|
|
185
|
+
projectId: id,
|
|
186
|
+
timestamp: Date.now(),
|
|
187
|
+
});
|
|
120
188
|
return id || null;
|
|
121
189
|
};
|
|
122
190
|
|
|
123
|
-
const projCache = readJsonMigrate(
|
|
124
|
-
|
|
191
|
+
const projCache = readJsonMigrate(
|
|
192
|
+
GEMINI_PROJECT_CACHE_PATH,
|
|
193
|
+
LEGACY_GEMINI_PROJECT_CACHE,
|
|
194
|
+
null,
|
|
195
|
+
);
|
|
196
|
+
let projectId =
|
|
197
|
+
projCache?.cacheKey === cacheKey ? projCache?.projectId : null;
|
|
125
198
|
if (!projectId) projectId = await fetchProjectId();
|
|
126
199
|
if (!projectId) return cache;
|
|
127
200
|
|
|
@@ -145,7 +218,11 @@ export async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
145
218
|
|
|
146
219
|
if (!quotaRes?.buckets) {
|
|
147
220
|
// API 응답에 buckets 없음: 에러 코드 또는 응답 내용을 캐시에 기록
|
|
148
|
-
const apiError =
|
|
221
|
+
const apiError =
|
|
222
|
+
quotaRes?.error?.message ||
|
|
223
|
+
quotaRes?.error?.code ||
|
|
224
|
+
quotaRes?.error ||
|
|
225
|
+
"no buckets in response";
|
|
149
226
|
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
150
227
|
...(cache || {}),
|
|
151
228
|
timestamp: cache?.timestamp || Date.now(),
|
|
@@ -175,12 +252,17 @@ export async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
175
252
|
export function readGeminiRpm(model) {
|
|
176
253
|
try {
|
|
177
254
|
// 새 경로 → 레거시 경로 fallback
|
|
178
|
-
const rpmPath = existsSync(GEMINI_RPM_TRACKER_PATH)
|
|
179
|
-
|
|
255
|
+
const rpmPath = existsSync(GEMINI_RPM_TRACKER_PATH)
|
|
256
|
+
? GEMINI_RPM_TRACKER_PATH
|
|
257
|
+
: existsSync(LEGACY_GEMINI_RPM_TRACKER)
|
|
258
|
+
? LEGACY_GEMINI_RPM_TRACKER
|
|
259
|
+
: null;
|
|
180
260
|
if (!rpmPath) return { count: 0, percent: 0, remainingSec: 60 };
|
|
181
261
|
const raw = readFileSync(rpmPath, "utf-8");
|
|
182
262
|
const parsed = JSON.parse(raw);
|
|
183
|
-
const timestamps = Array.isArray(parsed.timestamps)
|
|
263
|
+
const timestamps = Array.isArray(parsed.timestamps)
|
|
264
|
+
? parsed.timestamps
|
|
265
|
+
: [];
|
|
184
266
|
const now = Date.now();
|
|
185
267
|
const recent = timestamps.filter((t) => now - t < GEMINI_RPM_WINDOW_MS);
|
|
186
268
|
const count = recent.length;
|
|
@@ -188,9 +270,15 @@ export function readGeminiRpm(model) {
|
|
|
188
270
|
const percent = clampPercent(Math.round((count / rpmLimit) * 100));
|
|
189
271
|
// 가장 오래된 엔트리가 윈도우에서 빠지기까지 남은 초 (0건이면 0s)
|
|
190
272
|
// 5초 단위 반올림으로 HUD 깜빡임 감소
|
|
191
|
-
const rawRemainingSec =
|
|
192
|
-
|
|
193
|
-
|
|
273
|
+
const rawRemainingSec =
|
|
274
|
+
recent.length > 0
|
|
275
|
+
? Math.max(
|
|
276
|
+
0,
|
|
277
|
+
Math.ceil(
|
|
278
|
+
(GEMINI_RPM_WINDOW_MS - (now - Math.min(...recent))) / 1000,
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
: 0;
|
|
194
282
|
const remainingSec = Math.ceil(rawRemainingSec / 5) * 5;
|
|
195
283
|
return { count, percent, remainingSec };
|
|
196
284
|
} catch {
|
|
@@ -199,7 +287,11 @@ export function readGeminiRpm(model) {
|
|
|
199
287
|
}
|
|
200
288
|
|
|
201
289
|
export function readGeminiQuotaSnapshot(accountId, authContext) {
|
|
202
|
-
const cache = readJsonMigrate(
|
|
290
|
+
const cache = readJsonMigrate(
|
|
291
|
+
GEMINI_QUOTA_CACHE_PATH,
|
|
292
|
+
LEGACY_GEMINI_QUOTA_CACHE,
|
|
293
|
+
null,
|
|
294
|
+
);
|
|
203
295
|
if (!cache?.buckets) {
|
|
204
296
|
return { quota: null, shouldRefresh: true };
|
|
205
297
|
}
|
|
@@ -208,19 +300,24 @@ export function readGeminiQuotaSnapshot(accountId, authContext) {
|
|
|
208
300
|
const isLegacyCache = !cache.cacheKey;
|
|
209
301
|
const keyMatched = cache.cacheKey === cacheKey;
|
|
210
302
|
const cacheTs = Number(cache.timestamp);
|
|
211
|
-
const ageMs = Number.isFinite(cacheTs)
|
|
303
|
+
const ageMs = Number.isFinite(cacheTs)
|
|
304
|
+
? Date.now() - cacheTs
|
|
305
|
+
: Number.MAX_SAFE_INTEGER;
|
|
212
306
|
const isFresh = ageMs < GEMINI_QUOTA_STALE_MS;
|
|
213
307
|
|
|
214
308
|
if (keyMatched) {
|
|
215
309
|
// resetTime이 지난 버킷의 remainingFraction을 1로 보정 (stale 캐시 방지)
|
|
216
310
|
if (Array.isArray(cache.buckets)) {
|
|
217
311
|
const now = Date.now();
|
|
218
|
-
const patchedBuckets = cache.buckets.map(b =>
|
|
312
|
+
const patchedBuckets = cache.buckets.map((b) =>
|
|
219
313
|
b?.resetTime && new Date(b.resetTime).getTime() <= now
|
|
220
314
|
? { ...b, remainingFraction: 1 }
|
|
221
|
-
: b
|
|
315
|
+
: b,
|
|
222
316
|
);
|
|
223
|
-
return {
|
|
317
|
+
return {
|
|
318
|
+
quota: { ...cache, buckets: patchedBuckets },
|
|
319
|
+
shouldRefresh: !isFresh,
|
|
320
|
+
};
|
|
224
321
|
}
|
|
225
322
|
return { quota: cache, shouldRefresh: !isFresh };
|
|
226
323
|
}
|
|
@@ -233,10 +330,28 @@ export function readGeminiQuotaSnapshot(accountId, authContext) {
|
|
|
233
330
|
export function scheduleGeminiQuotaRefresh(accountId) {
|
|
234
331
|
const scriptPath = process.argv[1];
|
|
235
332
|
if (!scriptPath) return;
|
|
333
|
+
|
|
334
|
+
// 스폰 락: 30초 내 이미 스폰했으면 중복 방지
|
|
335
|
+
try {
|
|
336
|
+
if (existsSync(GEMINI_QUOTA_REFRESH_LOCK_PATH)) {
|
|
337
|
+
const lockAge =
|
|
338
|
+
Date.now() - readJson(GEMINI_QUOTA_REFRESH_LOCK_PATH, {}).t;
|
|
339
|
+
if (lockAge < SPAWN_LOCK_TTL_MS) return;
|
|
340
|
+
}
|
|
341
|
+
writeJsonSafe(GEMINI_QUOTA_REFRESH_LOCK_PATH, { t: Date.now() });
|
|
342
|
+
} catch {
|
|
343
|
+
/* 락 실패 무시 — 스폰 진행 */
|
|
344
|
+
}
|
|
345
|
+
|
|
236
346
|
try {
|
|
237
347
|
const child = spawn(
|
|
238
348
|
process.execPath,
|
|
239
|
-
[
|
|
349
|
+
[
|
|
350
|
+
scriptPath,
|
|
351
|
+
GEMINI_REFRESH_FLAG,
|
|
352
|
+
"--account",
|
|
353
|
+
accountId || "gemini-main",
|
|
354
|
+
],
|
|
240
355
|
{
|
|
241
356
|
detached: true,
|
|
242
357
|
stdio: "ignore",
|
|
@@ -255,7 +370,11 @@ export function scheduleGeminiQuotaRefresh(accountId) {
|
|
|
255
370
|
}
|
|
256
371
|
|
|
257
372
|
export function readGeminiSessionSnapshot() {
|
|
258
|
-
const cache = readJsonMigrate(
|
|
373
|
+
const cache = readJsonMigrate(
|
|
374
|
+
GEMINI_SESSION_CACHE_PATH,
|
|
375
|
+
LEGACY_GEMINI_SESSION_CACHE,
|
|
376
|
+
null,
|
|
377
|
+
);
|
|
259
378
|
if (!cache?.session) {
|
|
260
379
|
return { session: null, shouldRefresh: true };
|
|
261
380
|
}
|
|
@@ -275,14 +394,33 @@ export function refreshGeminiSessionCache() {
|
|
|
275
394
|
export function scheduleGeminiSessionRefresh() {
|
|
276
395
|
const scriptPath = process.argv[1];
|
|
277
396
|
if (!scriptPath) return;
|
|
397
|
+
|
|
398
|
+
// 스폰 락: 30초 내 이미 스폰했으면 중복 방지
|
|
278
399
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
400
|
+
if (existsSync(GEMINI_SESSION_REFRESH_LOCK_PATH)) {
|
|
401
|
+
const lockAge =
|
|
402
|
+
Date.now() - readJson(GEMINI_SESSION_REFRESH_LOCK_PATH, {}).t;
|
|
403
|
+
if (lockAge < SPAWN_LOCK_TTL_MS) return;
|
|
404
|
+
}
|
|
405
|
+
writeJsonSafe(GEMINI_SESSION_REFRESH_LOCK_PATH, { t: Date.now() });
|
|
406
|
+
} catch {
|
|
407
|
+
/* 락 실패 무시 — 스폰 진행 */
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const child = spawn(
|
|
412
|
+
process.execPath,
|
|
413
|
+
[scriptPath, GEMINI_SESSION_REFRESH_FLAG],
|
|
414
|
+
{
|
|
415
|
+
detached: true,
|
|
416
|
+
stdio: "ignore",
|
|
417
|
+
windowsHide: true,
|
|
418
|
+
},
|
|
419
|
+
);
|
|
284
420
|
child.unref();
|
|
285
|
-
} catch {
|
|
421
|
+
} catch {
|
|
422
|
+
/* 백그라운드 실행 실패 무시 */
|
|
423
|
+
}
|
|
286
424
|
}
|
|
287
425
|
|
|
288
426
|
// ============================================================================
|
|
@@ -294,27 +432,47 @@ export function scanGeminiSessionTokens() {
|
|
|
294
432
|
let best = null;
|
|
295
433
|
let bestTime = 0;
|
|
296
434
|
try {
|
|
297
|
-
const dirs = readdirSync(tmpDir).filter((d) =>
|
|
435
|
+
const dirs = readdirSync(tmpDir).filter((d) =>
|
|
436
|
+
existsSync(join(tmpDir, d, "chats")),
|
|
437
|
+
);
|
|
298
438
|
for (const dir of dirs) {
|
|
299
439
|
const chatsDir = join(tmpDir, dir, "chats");
|
|
300
440
|
let files;
|
|
301
|
-
try {
|
|
441
|
+
try {
|
|
442
|
+
files = readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
|
|
443
|
+
} catch {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
302
446
|
for (const file of files) {
|
|
303
447
|
try {
|
|
304
448
|
const data = JSON.parse(readFileSync(join(chatsDir, file), "utf-8"));
|
|
305
449
|
const updatedAt = new Date(data.lastUpdated || 0).getTime();
|
|
306
450
|
if (updatedAt <= bestTime) continue;
|
|
307
|
-
let input = 0,
|
|
451
|
+
let input = 0,
|
|
452
|
+
output = 0;
|
|
308
453
|
let model = "unknown";
|
|
309
454
|
for (const msg of data.messages || []) {
|
|
310
|
-
if (msg.tokens) {
|
|
455
|
+
if (msg.tokens) {
|
|
456
|
+
input += msg.tokens.input || 0;
|
|
457
|
+
output += msg.tokens.output || 0;
|
|
458
|
+
}
|
|
311
459
|
if (msg.model) model = msg.model;
|
|
312
460
|
}
|
|
313
461
|
bestTime = updatedAt;
|
|
314
|
-
best = {
|
|
315
|
-
|
|
462
|
+
best = {
|
|
463
|
+
input,
|
|
464
|
+
output,
|
|
465
|
+
total: input + output,
|
|
466
|
+
model,
|
|
467
|
+
lastUpdated: data.lastUpdated,
|
|
468
|
+
};
|
|
469
|
+
} catch {
|
|
470
|
+
/* 무시 */
|
|
471
|
+
}
|
|
316
472
|
}
|
|
317
473
|
}
|
|
318
|
-
} catch {
|
|
474
|
+
} catch {
|
|
475
|
+
/* 무시 */
|
|
476
|
+
}
|
|
319
477
|
return best;
|
|
320
478
|
}
|