@triflux/core 10.51.0 → 10.53.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.
Files changed (61) hide show
  1. package/hub/bridge.mjs +69 -1197
  2. package/hub/cli-adapter-base.mjs +4 -104
  3. package/hub/codex-adapter.mjs +7 -116
  4. package/hub/index.mjs +3 -20
  5. package/hub/lib/process-utils.mjs +1 -33
  6. package/hub/team/claude-daemon-control.mjs +1 -2
  7. package/hub/team/claude-session-projection.mjs +5 -73
  8. package/hub/team/claude-transcript.mjs +19 -0
  9. package/hub/team/nativeProxy.mjs +759 -0
  10. package/hub/team/session-context.mjs +1 -1
  11. package/hud/constants.mjs +9 -22
  12. package/hud/hud-qos-status.mjs +16 -19
  13. package/hud/providers/codex-probe.mjs +2 -31
  14. package/hud/providers/codex.mjs +16 -58
  15. package/hud/providers/gemini.mjs +20 -0
  16. package/hud/renderers.mjs +101 -77
  17. package/hud/utils.mjs +9 -13
  18. package/package.json +3 -6
  19. package/scripts/lib/agent-route-policy.mjs +0 -67
  20. package/scripts/lib/cli-claude.mjs +0 -12
  21. package/scripts/lib/env-probe.mjs +0 -70
  22. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  23. package/scripts/lib/mcp-guard-engine.mjs +42 -101
  24. package/scripts/lib/session-presence.mjs +1 -99
  25. package/hub/account-broker.mjs +0 -1221
  26. package/hub/adaptive-diagnostic.mjs +0 -350
  27. package/hub/adaptive-inject.mjs +0 -224
  28. package/hub/adaptive-memory.mjs +0 -277
  29. package/hub/adaptive.mjs +0 -172
  30. package/hub/assign-callbacks.mjs +0 -170
  31. package/hub/delegator/contracts.mjs +0 -38
  32. package/hub/delegator/index.mjs +0 -14
  33. package/hub/delegator/schema/delegator-tools.schema.json +0 -250
  34. package/hub/delegator/service.mjs +0 -336
  35. package/hub/delegator/tool-definitions.mjs +0 -35
  36. package/hub/hitl.mjs +0 -202
  37. package/hub/lib/known-errors.json +0 -72
  38. package/hub/lib/memory-store.mjs +0 -1001
  39. package/hub/lib/phase-manager.mjs +0 -493
  40. package/hub/lib/state-snapshot.mjs +0 -293
  41. package/hub/lib/trace-recorder.mjs +0 -153
  42. package/hub/lib/uuidv7.mjs +0 -45
  43. package/hub/memory-doctor.mjs +0 -640
  44. package/hub/middleware/quota-middleware.mjs +0 -70
  45. package/hub/middleware/request-logger.mjs +0 -191
  46. package/hub/paths.mjs +0 -24
  47. package/hub/pipeline/index.mjs +0 -163
  48. package/hub/pipeline/state.mjs +0 -191
  49. package/hub/pipeline/transitions.mjs +0 -131
  50. package/hub/reflexion.mjs +0 -290
  51. package/hub/role-contract.mjs +0 -312
  52. package/hub/router.mjs +0 -2026
  53. package/hub/session-fingerprint.mjs +0 -418
  54. package/hub/state.mjs +0 -300
  55. package/hub/team/synapse-http.mjs +0 -311
  56. package/hub/team-bridge.mjs +0 -27
  57. package/scripts/lib/claudemd-scanner.mjs +0 -26
  58. package/scripts/lib/context.mjs +0 -51
  59. package/scripts/lib/handoff.mjs +0 -187
  60. package/scripts/lib/logger.mjs +0 -122
  61. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -96,7 +96,7 @@ export function contextGuard(
96
96
  : {};
97
97
  }
98
98
 
99
- async function findCodexRollout(threadId) {
99
+ export async function findCodexRollout(threadId) {
100
100
  if (!/^[a-zA-Z0-9-]+$/u.test(threadId || "")) return null;
101
101
  const root = path.join(
102
102
  process.env.CODEX_HOME || path.join(homedir(), ".codex"),
package/hud/constants.mjs CHANGED
@@ -6,21 +6,6 @@ import { join } from "node:path";
6
6
 
7
7
  export const VERSION = "2.0";
8
8
 
9
- export const ACCOUNTS_CONFIG_PATH = join(
10
- homedir(),
11
- ".claude",
12
- "cache",
13
- "tfx-hub",
14
- "accounts.json",
15
- );
16
- export const ACCOUNTS_STATE_PATH = join(
17
- homedir(),
18
- ".claude",
19
- "cache",
20
- "tfx-hub",
21
- "cli_accounts_state.json",
22
- );
23
-
24
9
  // triflux-mods 의 Claude band 가 세션마다 갱신하는 표식. 있으면 Claude 행을 뺀다.
25
10
  export const CLAUDE_BAND_MARKER_DIR = join(
26
11
  homedir(),
@@ -107,19 +92,19 @@ export const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000;
107
92
  export const ONE_DAY_MS = 24 * 60 * 60 * 1000;
108
93
  export const DEFAULT_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
109
94
 
110
- export const CODEX_AUTH_PATH = join(homedir(), ".codex", "auth.json");
95
+ // Codex CLI 는 CODEX_HOME 이 있으면 그 디렉터리를 쓴다. 호출 시점에 읽어 환경 변화를 따른다.
96
+ export function getCodexHome() {
97
+ return process.env.CODEX_HOME || join(homedir(), ".codex");
98
+ }
99
+ export function getCodexAuthPath() {
100
+ return join(getCodexHome(), "auth.json");
101
+ }
111
102
  export const CODEX_PROBE_STATE_PATH = join(
112
103
  homedir(),
113
104
  ".claude",
114
105
  "cache",
115
106
  "codex-probe-state.json",
116
107
  );
117
- export const CODEX_BROKER_AUTH_CACHE_DIR = join(
118
- homedir(),
119
- ".claude",
120
- "cache",
121
- "tfx-hub",
122
- );
123
108
  export const CODEX_PROBE_TTL_MS = 5 * 60 * 1000;
124
109
  export const CODEX_PROBE_TIMEOUT_MS = 10_000;
125
110
  export const CODEX_QUOTA_CACHE_PATH = join(
@@ -171,6 +156,8 @@ export const ANTIGRAVITY_REFRESH_LOCK_PATH = join(
171
156
  ".antigravity-refresh-lock",
172
157
  );
173
158
  export const ANTIGRAVITY_QUOTA_STALE_MS = 5 * 60 * 1000;
159
+ // 조회가 잠시 실패해도 이 시간 안에 성공한 적이 있으면 로그인 상태로 본다.
160
+ export const ANTIGRAVITY_SIGNED_IN_GRACE_MS = 60 * 60 * 1000;
174
161
  export const ANTIGRAVITY_REFRESH_FLAG = "--refresh-antigravity-quota";
175
162
  export const ACCOUNT_LABEL_WIDTH = 10;
176
163
  export const PROVIDER_PREFIX_WIDTH = 2;
@@ -14,13 +14,13 @@ import {
14
14
  RESET,
15
15
  } from "./colors.mjs";
16
16
  import {
17
- ACCOUNTS_CONFIG_PATH,
18
- ACCOUNTS_STATE_PATH,
19
17
  ANTIGRAVITY_REFRESH_FLAG,
20
18
  CLAUDE_BAND_MARKER_DIR,
21
19
  CLAUDE_BAND_MARKER_TTL_MS,
22
20
  CLAUDE_REFRESH_FLAG,
23
21
  CODEX_REFRESH_FLAG,
22
+ getCodexAuthPath,
23
+ getCodexHome,
24
24
  } from "./constants.mjs";
25
25
  import { buildContextUsageView } from "./context-monitor.mjs";
26
26
  import {
@@ -30,13 +30,13 @@ import {
30
30
  } from "./providers/claude.mjs";
31
31
  import {
32
32
  getCodexEmail,
33
- hasBrokerCodexAccounts,
34
33
  readCodexRateLimitSnapshot,
35
34
  refreshCodexRateLimitsCache,
36
35
  scheduleCodexRateLimitRefresh,
37
36
  } from "./providers/codex.mjs";
38
37
  import {
39
38
  getAntigravityAccountLabel,
39
+ getAntigravityAuthKind,
40
40
  readAntigravityQuotaSnapshot,
41
41
  refreshAntigravityQuotaCache,
42
42
  scheduleAntigravityQuotaRefresh,
@@ -48,7 +48,7 @@ import {
48
48
  renderAlignedRows,
49
49
  } from "./renderers.mjs";
50
50
  import { selectTier } from "./terminal.mjs";
51
- import { readJson, readStdinJson } from "./utils.mjs";
51
+ import { formatTimeCell, formatTimeCellDH, readStdinJson } from "./utils.mjs";
52
52
 
53
53
  async function main() {
54
54
  if (process.argv.includes(CLAUDE_REFRESH_FLAG)) {
@@ -66,8 +66,6 @@ async function main() {
66
66
 
67
67
  const stdinPromise = readStdinJson();
68
68
  const { showCodex, antigravityAllowed } = resolveHudCliVisibility();
69
- const accountsConfig = readJson(ACCOUNTS_CONFIG_PATH, { providers: {} });
70
- const accountsState = readJson(ACCOUNTS_STATE_PATH, { providers: {} });
71
69
  const claudeUsageSnapshot = readClaudeUsageSnapshot();
72
70
  const codexSnapshot = readCodexRateLimitSnapshot();
73
71
  const antigravitySnapshot = antigravityAllowed
@@ -81,11 +79,7 @@ async function main() {
81
79
  ) {
82
80
  scheduleClaudeUsageRefresh();
83
81
  }
84
- if (
85
- showCodex &&
86
- codexSnapshot.shouldRefresh &&
87
- (existsSync(join(homedir(), ".codex")) || hasBrokerCodexAccounts())
88
- ) {
82
+ if (showCodex && codexSnapshot.shouldRefresh && existsSync(getCodexHome())) {
89
83
  scheduleCodexRateLimitRefresh();
90
84
  }
91
85
 
@@ -95,12 +89,19 @@ async function main() {
95
89
  ? { ...claudeUsageSnapshot.data, stale: claudeUsageSnapshot.isStale }
96
90
  : null;
97
91
  const codexBuckets = codexSnapshot.buckets;
92
+ const antigravityQuota = antigravityAllowed
93
+ ? { ...antigravitySnapshot?.data, auth: getAntigravityAuthKind() }
94
+ : null;
98
95
  const currentTier = selectTier();
96
+ // 회색은 로그인 안 된 경우에만 쓴다. 로그인 상태의 조회 공백은 --% 로만 보인다.
97
+ const codexLoggedOut =
98
+ showCodex && !codexBuckets && !existsSync(getCodexAuthPath());
99
99
  if (currentTier === "nano") {
100
100
  const microLine = getMicroLine(contextView, claudeUsage, codexBuckets, {
101
101
  showCodex,
102
+ codexLoggedOut,
102
103
  showAntigravity: antigravityAllowed,
103
- antigravityQuota: antigravitySnapshot?.data,
104
+ antigravityQuota,
104
105
  });
105
106
  process.stdout.write(`\x1b[0m${microLine}\n`);
106
107
  return;
@@ -118,8 +119,6 @@ async function main() {
118
119
  "codex",
119
120
  "x",
120
121
  codexWhite,
121
- accountsConfig,
122
- accountsState,
123
122
  codexBuckets ? { type: "codex", buckets: codexBuckets } : null,
124
123
  getCodexEmail(),
125
124
  ),
@@ -132,16 +131,14 @@ async function main() {
132
131
  "antigravity",
133
132
  "a",
134
133
  geminiBlue,
135
- accountsConfig,
136
- accountsState,
137
- antigravitySnapshot?.data,
134
+ antigravityQuota,
138
135
  getAntigravityAccountLabel(),
139
136
  ),
140
137
  );
141
138
  }
142
139
 
143
140
  const outputLines = renderAlignedRows(rows);
144
- if (!codexBuckets && outputLines[codexRowIndex] != null) {
141
+ if (outputLines[codexRowIndex] != null && codexLoggedOut) {
145
142
  outputLines[codexRowIndex] = `${DIM}${outputLines[codexRowIndex]}${RESET}`;
146
143
  }
147
144
  // 알림 배너와 TUI 스타일이 HUD 내용에 겹치지 않도록 한다.
@@ -152,7 +149,7 @@ async function main() {
152
149
 
153
150
  main().catch(() => {
154
151
  process.stdout.write(
155
- `\x1b[0m${bold(claudeOrange("c"))}: ${dim("5h:--% (n/a) 1w:--% (n/a) | ctx:--%")}\n`,
152
+ `\x1b[0m${bold(claudeOrange("c"))}: ${dim(`5h:--% ${formatTimeCell("")} 1w:--% ${formatTimeCellDH("")} | CTX:--%`)}\n`,
156
153
  );
157
154
  });
158
155
 
@@ -2,16 +2,8 @@
2
2
  // 세션 이벤트(snake_case) 파이프라인에 합류 가능한 스냅샷을 만든다.
3
3
 
4
4
  import { spawn } from "node:child_process";
5
- import {
6
- existsSync,
7
- mkdirSync,
8
- mkdtempSync,
9
- readdirSync,
10
- readFileSync,
11
- writeFileSync,
12
- } from "node:fs";
13
- import { tmpdir } from "node:os";
14
- import { dirname, join } from "node:path";
5
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { dirname } from "node:path";
15
7
 
16
8
  function readJwtExpSec(token) {
17
9
  try {
@@ -54,7 +46,6 @@ export function recordProbe(stateFilePath, key, nowMs) {
54
46
 
55
47
  export function listProbeTargets({
56
48
  codexAuthPath,
57
- brokerCacheDir,
58
49
  stateFilePath,
59
50
  ttlMs,
60
51
  nowMs,
@@ -68,26 +59,6 @@ export function listProbeTargets({
68
59
  if (currentAuth && accessTokenUsable(currentAuth, nowMs) && due("current")) {
69
60
  targets.push({ key: "current", codexHome: dirname(codexAuthPath) });
70
61
  }
71
-
72
- if (brokerCacheDir && existsSync(brokerCacheDir)) {
73
- let files = [];
74
- try {
75
- files = readdirSync(brokerCacheDir).filter((file) =>
76
- /^codex-auth-.+\.json$/.test(file),
77
- );
78
- } catch {
79
- files = [];
80
- }
81
- for (const file of files) {
82
- const key = file.replace(/^codex-auth-/, "").replace(/\.json$/, "");
83
- if (!due(key)) continue;
84
- const auth = readJsonFile(join(brokerCacheDir, file));
85
- if (!auth || !accessTokenUsable(auth, nowMs)) continue;
86
- const home = mkdtempSync(join(tmpdir(), `tfx-probe-home-${key}-`));
87
- writeFileSync(join(home, "auth.json"), JSON.stringify(auth));
88
- targets.push({ key, codexHome: home });
89
- }
90
- }
91
62
  return targets;
92
63
  }
93
64
 
@@ -4,11 +4,8 @@
4
4
 
5
5
  import { spawn } from "node:child_process";
6
6
  import { existsSync, readdirSync, readFileSync } from "node:fs";
7
- import { homedir } from "node:os";
8
7
  import { join } from "node:path";
9
8
  import {
10
- CODEX_AUTH_PATH,
11
- CODEX_BROKER_AUTH_CACHE_DIR,
12
9
  CODEX_MIN_BUCKETS,
13
10
  CODEX_PROBE_STATE_PATH,
14
11
  CODEX_PROBE_TIMEOUT_MS,
@@ -17,6 +14,8 @@ import {
17
14
  CODEX_QUOTA_STALE_MS,
18
15
  CODEX_REFRESH_FLAG,
19
16
  CODEX_REFRESH_LOCK_PATH,
17
+ getCodexAuthPath,
18
+ getCodexHome,
20
19
  SPAWN_LOCK_TTL_MS,
21
20
  } from "../constants.mjs";
22
21
  import { decodeJwtEmail, readJson, writeJsonSafe } from "../utils.mjs";
@@ -59,7 +58,7 @@ export function normalizeBuckets(rl) {
59
58
 
60
59
  export function getCodexEmail() {
61
60
  try {
62
- const auth = JSON.parse(readFileSync(CODEX_AUTH_PATH, "utf-8"));
61
+ const auth = JSON.parse(readFileSync(getCodexAuthPath(), "utf-8"));
63
62
  return decodeJwtEmail(auth?.tokens?.id_token);
64
63
  } catch {
65
64
  return null;
@@ -117,8 +116,8 @@ function positiveWindowMinutes(bucket) {
117
116
  return minutes > 0 ? minutes : null;
118
117
  }
119
118
 
120
- // 계정 하나에는 같은 종류의 창이 한 번에 하나만 열린다. 브로커 계정이 없으면
121
- // ~/.codex/sessions 와 HUD 라벨이 모두 ~/.codex/auth.json 계정 하나의 것이다. 그래서
119
+ // 계정 하나에는 같은 종류의 창이 한 번에 하나만 열린다. Codex 홈(CODEX_HOME)의
120
+ // sessions 와 HUD 라벨은 모두 auth.json 계정 하나의 것이다. 그래서
122
121
  // 세션 로그에 더 늦게 리셋되는 창이 나타났고 옛 창이 그 창이 열리기 전에만 관측됐다면,
123
122
  // 옛 창은 조기 리셋(2026-09-23 실측)이나 계정 전환으로 끝난 창이다. 프로브로만 본 창은
124
123
  // 다른 계정일 수 있어 근거로 쓰지 않는다. 새 창이 열린 뒤에도 관측된 창은 함께 도는
@@ -135,12 +134,7 @@ function isReplacedWindowGroup(group, other) {
135
134
  return lastSeenMs <= otherOpenedAtMs;
136
135
  }
137
136
 
138
- function selectCodexWindowSnapshot(
139
- snapshots,
140
- nowSec,
141
- windowKey,
142
- replaceEndedWindows,
143
- ) {
137
+ function selectCodexWindowSnapshot(snapshots, nowSec, windowKey) {
144
138
  const groups = [];
145
139
  const ungrouped = [];
146
140
 
@@ -179,12 +173,9 @@ function selectCodexWindowSnapshot(
179
173
  const openGroups = groups.filter(
180
174
  (group) => Number(group.latest[windowKey]?.resets_at) > nowSec,
181
175
  );
182
- const activeGroups = replaceEndedWindows
183
- ? openGroups.filter(
184
- (group) =>
185
- !openGroups.some((other) => isReplacedWindowGroup(group, other)),
186
- )
187
- : openGroups;
176
+ const activeGroups = openGroups.filter(
177
+ (group) => !openGroups.some((other) => isReplacedWindowGroup(group, other)),
178
+ );
188
179
  let selected = null;
189
180
  if (activeGroups.length > 0) {
190
181
  selected = activeGroups
@@ -214,18 +205,16 @@ function selectCodexWindowSnapshot(
214
205
  return { snapshot: selected, activeGroupCount: activeGroups.length };
215
206
  }
216
207
 
217
- function selectCodexSnapshot(snapshots, nowSec, replaceEndedWindows) {
208
+ function selectCodexSnapshot(snapshots, nowSec) {
218
209
  const primarySelection = selectCodexWindowSnapshot(
219
210
  snapshots,
220
211
  nowSec,
221
212
  "primary",
222
- replaceEndedWindows,
223
213
  );
224
214
  const secondarySelection = selectCodexWindowSnapshot(
225
215
  snapshots,
226
216
  nowSec,
227
217
  "secondary",
228
- replaceEndedWindows,
229
218
  );
230
219
  const primarySnapshot = primarySelection.snapshot;
231
220
  if (!primarySnapshot) return null;
@@ -246,7 +235,7 @@ function selectCodexSnapshot(snapshots, nowSec, replaceEndedWindows) {
246
235
  return selected;
247
236
  }
248
237
 
249
- function mergeRateLimitSnapshots(snapshots, nowSec, replaceEndedWindows) {
238
+ function mergeRateLimitSnapshots(snapshots, nowSec) {
250
239
  const merged = {};
251
240
  const codexSnapshots = [];
252
241
  for (const snapshot of snapshots) {
@@ -259,11 +248,7 @@ function mergeRateLimitSnapshots(snapshots, nowSec, replaceEndedWindows) {
259
248
  }
260
249
  }
261
250
 
262
- const codex = selectCodexSnapshot(
263
- codexSnapshots,
264
- nowSec,
265
- replaceEndedWindows,
266
- );
251
+ const codex = selectCodexSnapshot(codexSnapshots, nowSec);
267
252
  if (codex) merged.codex = codex;
268
253
  return merged;
269
254
  }
@@ -275,12 +260,10 @@ function mergeRateLimitSnapshots(snapshots, nowSec, replaceEndedWindows) {
275
260
  // 합성 버킷(token_count 기반)은 2일 이내 데이터만 허용하여 stale 방지.
276
261
  // ============================================================================
277
262
  export function getCodexRateLimits({
278
- sessionsRoot = join(homedir(), ".codex", "sessions"),
263
+ sessionsRoot = join(getCodexHome(), "sessions"),
279
264
  now = new Date(),
280
265
  maxLinesPerFile = 800,
281
266
  extraSnapshots = [],
282
- // 교체된 창 판정(isReplacedWindowGroup). 브로커 Codex 계정이 있으면 끈다.
283
- replaceEndedWindows = true,
284
267
  } = {}) {
285
268
  let syntheticBucket = null; // 최근 token_count에서 합성 (행 활성화 + 토큰 데이터용)
286
269
  // 프로브도 세션 로그와 같은 window_minutes 기준으로 5h/1w 슬롯을 정한다.
@@ -372,11 +355,7 @@ export function getCodexRateLimits({
372
355
  }
373
356
 
374
357
  const snapshots = dayOffset <= 1 ? recentSnapshots : daySnapshots;
375
- const mergedBuckets = mergeRateLimitSnapshots(
376
- snapshots,
377
- nowSec,
378
- replaceEndedWindows,
379
- );
358
+ const mergedBuckets = mergeRateLimitSnapshots(snapshots, nowSec);
380
359
  if (Object.keys(mergedBuckets).length > 0) {
381
360
  if (syntheticBucket) {
382
361
  const main =
@@ -407,8 +386,7 @@ export async function collectProbeSnapshots(now = new Date()) {
407
386
  let targets = [];
408
387
  try {
409
388
  targets = listProbeTargets({
410
- codexAuthPath: CODEX_AUTH_PATH,
411
- brokerCacheDir: CODEX_BROKER_AUTH_CACHE_DIR,
389
+ codexAuthPath: getCodexAuthPath(),
412
390
  stateFilePath: CODEX_PROBE_STATE_PATH,
413
391
  ttlMs: Number(process.env.TFX_CODEX_PROBE_TTL_MS) || CODEX_PROBE_TTL_MS,
414
392
  nowMs: now.getTime(),
@@ -432,30 +410,10 @@ export async function collectProbeSnapshots(now = new Date()) {
432
410
  );
433
411
  }
434
412
 
435
- // 브로커 계정은 자기 CODEX_HOME 에서 돌기 때문에, 옛 창이 전환 전 계정의 창이어도 그
436
- // 계정이 아직 쓰이고 있을 수 있다. accounts.json 의 codex 항목과 프로브용 인증 캐시로 판정한다.
437
- export function hasBrokerCodexAccounts({
438
- brokerDir = CODEX_BROKER_AUTH_CACHE_DIR,
439
- } = {}) {
440
- const accounts = readJson(join(brokerDir, "accounts.json"), null);
441
- if (Array.isArray(accounts?.codex) && accounts.codex.length > 0) return true;
442
- try {
443
- return readdirSync(brokerDir).some((file) =>
444
- /^codex-auth-.+\.json$/.test(file),
445
- );
446
- } catch {
447
- return false;
448
- }
449
- }
450
-
451
413
  export async function refreshCodexRateLimitsCache() {
452
414
  const now = new Date();
453
415
  const extraSnapshots = await collectProbeSnapshots(now);
454
- const buckets = getCodexRateLimits({
455
- now,
456
- extraSnapshots,
457
- replaceEndedWindows: !hasBrokerCodexAccounts(),
458
- });
416
+ const buckets = getCodexRateLimits({ now, extraSnapshots });
459
417
  // buckets가 null이어도 캐시 갱신 (stale 데이터 제거)
460
418
  writeJsonSafe(CODEX_QUOTA_CACHE_PATH, { timestamp: Date.now(), buckets });
461
419
  return buckets;
@@ -7,6 +7,7 @@ import {
7
7
  ANTIGRAVITY_REFRESH_FLAG,
8
8
  ANTIGRAVITY_REFRESH_LOCK_PATH,
9
9
  ANTIGRAVITY_SETTINGS_PATH,
10
+ ANTIGRAVITY_SIGNED_IN_GRACE_MS,
10
11
  SPAWN_LOCK_TTL_MS,
11
12
  } from "../constants.mjs";
12
13
  import { decodeJwtEmail, readJson, writeJsonSafe } from "../utils.mjs";
@@ -41,6 +42,18 @@ function isGcpProjectAuth() {
41
42
  return Boolean(readJson(ANTIGRAVITY_SETTINGS_PATH, null)?.gcp?.project);
42
43
  }
43
44
 
45
+ // Keychain 인증은 파일을 남기지 않으므로 최근 쿼터 조회 성공도 로그인 근거로 본다.
46
+ export function getAntigravityAuthKind() {
47
+ if (isGcpProjectAuth()) return "project";
48
+ if (getAntigravityAccountLabel()) return "account";
49
+ const cache = readJson(ANTIGRAVITY_QUOTA_CACHE_PATH, null);
50
+ const signedInAge = Date.now() - Number(cache?.signedInAt);
51
+ return cache?.accountLabel == null &&
52
+ signedInAge < ANTIGRAVITY_SIGNED_IN_GRACE_MS
53
+ ? "account"
54
+ : null;
55
+ }
56
+
44
57
  export function readAntigravityQuotaSnapshot() {
45
58
  // 프로젝트 과금의 기본 응답을 개인 플랜의 0% 사용률로 표시하지 않는다.
46
59
  if (isGcpProjectAuth()) return { data: null, shouldRefresh: false };
@@ -103,10 +116,17 @@ export function refreshAntigravityQuotaCache(execFileSyncFn = execFileSync) {
103
116
  // 실패도 캐시해 인증 오류나 CLI 부재 시 매 렌더마다 실행하지 않는다.
104
117
  }
105
118
  if (accountLabel === getAntigravityAccountLabel()) {
119
+ const previous = readJson(ANTIGRAVITY_QUOTA_CACHE_PATH, null);
120
+ const signedInAt = buckets
121
+ ? Date.now()
122
+ : previous?.accountLabel === accountLabel
123
+ ? previous?.signedInAt
124
+ : undefined;
106
125
  writeJsonSafe(ANTIGRAVITY_QUOTA_CACHE_PATH, {
107
126
  timestamp: Date.now(),
108
127
  accountLabel,
109
128
  buckets,
129
+ signedInAt,
110
130
  });
111
131
  }
112
132
  return buckets;