@triflux/core 10.50.2 → 10.52.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 (84) hide show
  1. package/hooks/agy-session-hook.mjs +11 -56
  2. package/hooks/codex-session-hook.mjs +11 -101
  3. package/hub/bridge.mjs +65 -1250
  4. package/hub/cli-adapter-base.mjs +12 -90
  5. package/hub/codex-adapter.mjs +0 -1
  6. package/hub/codex-preflight.mjs +5 -28
  7. package/hub/index.mjs +3 -19
  8. package/hub/lib/process-utils.mjs +1 -156
  9. package/hub/lib/spawn-trace.mjs +3 -2
  10. package/hub/lib/timeout-defaults.mjs +0 -1
  11. package/hub/team/claude-daemon-control.mjs +1 -2
  12. package/hub/team/claude-session-projection.mjs +5 -73
  13. package/hub/team/nativeProxy.mjs +759 -0
  14. package/hub/team/session-context.mjs +1 -1
  15. package/hud/cli-policy.mjs +0 -22
  16. package/hud/constants.mjs +13 -26
  17. package/hud/hud-qos-status.mjs +42 -54
  18. package/hud/providers/gemini.mjs +125 -20
  19. package/hud/renderers.mjs +101 -122
  20. package/hud/utils.mjs +10 -6
  21. package/package.json +2 -6
  22. package/scripts/lib/agent-route-policy.mjs +143 -62
  23. package/scripts/lib/cli-claude.mjs +0 -14
  24. package/scripts/lib/env-probe.mjs +2 -131
  25. package/scripts/lib/gemini-profiles.mjs +2 -69
  26. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  27. package/scripts/lib/machine-profile.mjs +3 -17
  28. package/scripts/lib/mcp-filter.mjs +109 -53
  29. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  30. package/scripts/lib/mcp-health.mjs +2 -2
  31. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  32. package/scripts/lib/psmux-info.mjs +11 -59
  33. package/scripts/lib/session-presence.mjs +2 -100
  34. package/scripts/lib/skill-template.mjs +0 -245
  35. package/hub/adaptive-diagnostic.mjs +0 -350
  36. package/hub/adaptive-inject.mjs +0 -224
  37. package/hub/adaptive-memory.mjs +0 -277
  38. package/hub/adaptive.mjs +0 -172
  39. package/hub/assign-callbacks.mjs +0 -170
  40. package/hub/delegator/contracts.mjs +0 -38
  41. package/hub/delegator/index.mjs +0 -14
  42. package/hub/delegator/schema/delegator-tools.schema.json +0 -250
  43. package/hub/delegator/service.mjs +0 -336
  44. package/hub/delegator/tool-definitions.mjs +0 -35
  45. package/hub/dynamic-routing-engine.mjs +0 -511
  46. package/hub/hitl.mjs +0 -202
  47. package/hub/lib/known-errors.json +0 -72
  48. package/hub/lib/memory-store.mjs +0 -1001
  49. package/hub/lib/phase-manager.mjs +0 -493
  50. package/hub/lib/risk-tier.mjs +0 -53
  51. package/hub/lib/ssh-command.mjs +0 -231
  52. package/hub/lib/staged-file-detect.mjs +0 -43
  53. package/hub/lib/state-snapshot.mjs +0 -293
  54. package/hub/lib/tfx-route-args.mjs +0 -353
  55. package/hub/lib/trace-recorder.mjs +0 -153
  56. package/hub/lib/uuidv7.mjs +0 -45
  57. package/hub/memory-doctor.mjs +0 -640
  58. package/hub/middleware/quota-middleware.mjs +0 -70
  59. package/hub/middleware/request-logger.mjs +0 -191
  60. package/hub/paths.mjs +0 -24
  61. package/hub/pipeline/index.mjs +0 -163
  62. package/hub/pipeline/state.mjs +0 -191
  63. package/hub/pipeline/transitions.mjs +0 -131
  64. package/hub/reflexion.mjs +0 -290
  65. package/hub/role-contract.mjs +0 -314
  66. package/hub/router.mjs +0 -2026
  67. package/hub/routing-snapshot.mjs +0 -261
  68. package/hub/session-fingerprint.mjs +0 -418
  69. package/hub/state.mjs +0 -300
  70. package/hub/team/synapse-http.mjs +0 -311
  71. package/hub/team-bridge.mjs +0 -27
  72. package/mesh/index.mjs +0 -69
  73. package/mesh/mesh-budget.mjs +0 -137
  74. package/mesh/mesh-heartbeat.mjs +0 -100
  75. package/mesh/mesh-protocol.mjs +0 -96
  76. package/mesh/mesh-queue.mjs +0 -163
  77. package/mesh/mesh-registry.mjs +0 -78
  78. package/mesh/mesh-router.mjs +0 -77
  79. package/scripts/lib/claudemd-scanner.mjs +0 -223
  80. package/scripts/lib/context.mjs +0 -67
  81. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  82. package/scripts/lib/handoff.mjs +0 -187
  83. package/scripts/lib/logger.mjs +0 -122
  84. 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"),
@@ -41,25 +41,3 @@ export function resolveHudCliVisibility(env = process.env, opts = {}) {
41
41
  return { showCodex: true, antigravityAllowed: true, policy: null };
42
42
  }
43
43
  }
44
-
45
- /**
46
- * antigravity / gemini 슬롯 행을 그릴지 판정한다.
47
- *
48
- * antigravity 가 허용이면 기존 동작 그대로 항상 그린다. 정책으로 꺼졌을 때만
49
- * gemini 폴백 정보가 하나라도 있는지 보고, 전부 비어 있으면 행을 생략한다.
50
- *
51
- * @param {{
52
- * antigravityAllowed?: boolean,
53
- * antigravityReady?: boolean,
54
- * geminiEmail?: unknown,
55
- * }} [args]
56
- * @returns {boolean}
57
- */
58
- export function shouldRenderGeminiFallbackRow({
59
- antigravityAllowed = true,
60
- antigravityReady = false,
61
- geminiEmail = null,
62
- } = {}) {
63
- if (antigravityAllowed || antigravityReady) return true;
64
- return Boolean(geminiEmail);
65
- }
package/hud/constants.mjs CHANGED
@@ -31,9 +31,6 @@ export const CLAUDE_BAND_MARKER_DIR = join(
31
31
  );
32
32
  export const CLAUDE_BAND_MARKER_TTL_MS = 12 * 60 * 60 * 1000;
33
33
 
34
- // tfx-multi 세션 상태 디렉터리
35
- export const TEAM_STATE_DIR =
36
- process.env.TFX_HUB_PID_DIR || join(homedir(), ".claude", "cache", "tfx-hub");
37
34
  export const CONTEXT_MONITOR_CACHE_PATH = join(
38
35
  homedir(),
39
36
  ".claude",
@@ -149,11 +146,9 @@ export const CODEX_REFRESH_LOCK_PATH = join(
149
146
  );
150
147
  export const SPAWN_LOCK_TTL_MS = 30 * 1000; // 30초 spawn dedup
151
148
 
152
- export const GEMINI_OAUTH_PATH = join(homedir(), ".gemini", "oauth_creds.json");
153
149
  export const ANTIGRAVITY_OAUTH_PATHS = [
154
150
  join(homedir(), ".gemini", "antigravity-cli", "oauth_creds.json"),
155
151
  join(homedir(), ".gemini", "antigravity-cli", "credentials.json"),
156
- GEMINI_OAUTH_PATH,
157
152
  ];
158
153
  export const ANTIGRAVITY_KEYCHAIN_SERVICE = "gemini";
159
154
  export const ANTIGRAVITY_KEYCHAIN_ACCOUNT = "antigravity";
@@ -163,33 +158,25 @@ export const ANTIGRAVITY_SETTINGS_PATH = join(
163
158
  "antigravity-cli",
164
159
  "settings.json",
165
160
  );
166
- // Antigravity CLI 현재 장착 모델 라벨 → HUD 2자 약어 매핑
167
- export const ANTIGRAVITY_MODEL_ABBREV = {
168
- // Flash 세대와 등급을 구분한다.
169
- "Gemini 3.8 Flash (High)": "8h",
170
- "Gemini 3.8 Flash (Medium)": "8m",
171
- "Gemini 3.8 Flash (Low)": "8l",
172
- "Gemini 3.7 Flash (High)": "7h",
173
- "Gemini 3.7 Flash (Medium)": "7m",
174
- "Gemini 3.7 Flash (Low)": "7l",
175
- "Gemini 3.6 Flash (High)": "6h",
176
- "Gemini 3.6 Flash (Medium)": "6m",
177
- "Gemini 3.6 Flash (Low)": "6l",
178
- "Gemini 3.5 Flash (High)": "Fh",
179
- "Gemini 3.5 Flash (Medium)": "Fm",
180
- "Gemini 3.5 Flash (Low)": "Fl",
181
- };
182
- export const TFX_PREFLIGHT_CACHE_PATH = join(
161
+ export const ANTIGRAVITY_QUOTA_CACHE_PATH = join(
183
162
  homedir(),
184
163
  ".claude",
185
164
  "cache",
186
- "tfx-preflight.json",
165
+ "antigravity-quota-cache.json",
187
166
  );
188
- export const TFX_PREFLIGHT_CACHE_STALE_MS = 60 * 60 * 1000;
189
-
167
+ export const ANTIGRAVITY_REFRESH_LOCK_PATH = join(
168
+ homedir(),
169
+ ".claude",
170
+ "cache",
171
+ ".antigravity-refresh-lock",
172
+ );
173
+ export const ANTIGRAVITY_QUOTA_STALE_MS = 5 * 60 * 1000;
174
+ // 조회가 잠시 실패해도 이 시간 안에 성공한 적이 있으면 로그인 상태로 본다.
175
+ export const ANTIGRAVITY_SIGNED_IN_GRACE_MS = 60 * 60 * 1000;
176
+ export const ANTIGRAVITY_REFRESH_FLAG = "--refresh-antigravity-quota";
190
177
  export const ACCOUNT_LABEL_WIDTH = 10;
191
178
  export const PROVIDER_PREFIX_WIDTH = 2;
192
- export const PERCENT_CELL_WIDTH = 3;
179
+ export const PERCENT_CELL_WIDTH = 4;
193
180
  export const TIME_CELL_INNER_WIDTH = 6;
194
181
 
195
182
  export const CLAUDE_REFRESH_FLAG = "--refresh-claude-usage";
@@ -3,10 +3,7 @@
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
- import {
7
- resolveHudCliVisibility,
8
- shouldRenderGeminiFallbackRow,
9
- } from "./cli-policy.mjs";
6
+ import { resolveHudCliVisibility } from "./cli-policy.mjs";
10
7
  import {
11
8
  bold,
12
9
  claudeOrange,
@@ -19,12 +16,12 @@ import {
19
16
  import {
20
17
  ACCOUNTS_CONFIG_PATH,
21
18
  ACCOUNTS_STATE_PATH,
19
+ ANTIGRAVITY_REFRESH_FLAG,
22
20
  CLAUDE_BAND_MARKER_DIR,
23
21
  CLAUDE_BAND_MARKER_TTL_MS,
24
22
  CLAUDE_REFRESH_FLAG,
23
+ CODEX_AUTH_PATH,
25
24
  CODEX_REFRESH_FLAG,
26
- TFX_PREFLIGHT_CACHE_PATH,
27
- TFX_PREFLIGHT_CACHE_STALE_MS,
28
25
  } from "./constants.mjs";
29
26
  import { buildContextUsageView } from "./context-monitor.mjs";
30
27
  import {
@@ -41,19 +38,24 @@ import {
41
38
  } from "./providers/codex.mjs";
42
39
  import {
43
40
  getAntigravityAccountLabel,
44
- getAntigravityCurrentModel,
45
- getAntigravityModelAbbrev,
46
- getGeminiEmail,
41
+ getAntigravityAuthKind,
42
+ readAntigravityQuotaSnapshot,
43
+ refreshAntigravityQuotaCache,
44
+ scheduleAntigravityQuotaRefresh,
47
45
  } from "./providers/gemini.mjs";
48
46
  import {
49
47
  getClaudeRows,
50
48
  getMicroLine,
51
49
  getProviderRow,
52
- getTeamRow,
53
50
  renderAlignedRows,
54
51
  } from "./renderers.mjs";
55
52
  import { selectTier } from "./terminal.mjs";
56
- import { readJson, readStdinJson } from "./utils.mjs";
53
+ import {
54
+ formatTimeCell,
55
+ formatTimeCellDH,
56
+ readJson,
57
+ readStdinJson,
58
+ } from "./utils.mjs";
57
59
 
58
60
  async function main() {
59
61
  if (process.argv.includes(CLAUDE_REFRESH_FLAG)) {
@@ -64,22 +66,21 @@ async function main() {
64
66
  await refreshCodexRateLimitsCache();
65
67
  return;
66
68
  }
69
+ if (process.argv.includes(ANTIGRAVITY_REFRESH_FLAG)) {
70
+ refreshAntigravityQuotaCache();
71
+ return;
72
+ }
67
73
 
68
74
  const stdinPromise = readStdinJson();
69
- const preflightCache = readJson(TFX_PREFLIGHT_CACHE_PATH, null);
70
- const preflightTimestamp = Number(preflightCache?.timestamp);
71
- const preflightFresh =
72
- Number.isFinite(preflightTimestamp) &&
73
- Date.now() - preflightTimestamp <= TFX_PREFLIGHT_CACHE_STALE_MS;
74
75
  const { showCodex, antigravityAllowed } = resolveHudCliVisibility();
75
- const antigravityReady =
76
- antigravityAllowed &&
77
- preflightFresh &&
78
- preflightCache?.antigravity?.ok === true;
79
76
  const accountsConfig = readJson(ACCOUNTS_CONFIG_PATH, { providers: {} });
80
77
  const accountsState = readJson(ACCOUNTS_STATE_PATH, { providers: {} });
81
78
  const claudeUsageSnapshot = readClaudeUsageSnapshot();
82
79
  const codexSnapshot = readCodexRateLimitSnapshot();
80
+ const antigravitySnapshot = antigravityAllowed
81
+ ? readAntigravityQuotaSnapshot()
82
+ : null;
83
+ if (antigravitySnapshot?.shouldRefresh) scheduleAntigravityQuotaRefresh();
83
84
  // 설정이 없는 홈에서는 갱신 프로세스를 시작하지 않는다.
84
85
  if (
85
86
  claudeUsageSnapshot.shouldRefresh &&
@@ -101,22 +102,16 @@ async function main() {
101
102
  ? { ...claudeUsageSnapshot.data, stale: claudeUsageSnapshot.isStale }
102
103
  : null;
103
104
  const codexBuckets = codexSnapshot.buckets;
105
+ const antigravityQuota = antigravityAllowed
106
+ ? { ...antigravitySnapshot?.data, auth: getAntigravityAuthKind() }
107
+ : null;
104
108
  const currentTier = selectTier();
105
- const geminiEmail = getGeminiEmail();
106
- const showGeminiRow = shouldRenderGeminiFallbackRow({
107
- antigravityAllowed,
108
- antigravityReady,
109
- geminiEmail,
110
- });
111
-
112
109
  if (currentTier === "nano") {
113
- const microLine = getMicroLine(
114
- contextView,
115
- claudeUsage,
116
- codexBuckets,
117
- antigravityReady,
118
- { showCodex, showGemini: showGeminiRow },
119
- );
110
+ const microLine = getMicroLine(contextView, claudeUsage, codexBuckets, {
111
+ showCodex,
112
+ showAntigravity: antigravityAllowed,
113
+ antigravityQuota,
114
+ });
120
115
  process.stdout.write(`\x1b[0m${microLine}\n`);
121
116
  return;
122
117
  }
@@ -140,38 +135,31 @@ async function main() {
140
135
  ),
141
136
  );
142
137
  }
143
- let geminiRowIndex = -1;
144
- if (showGeminiRow) {
145
- geminiRowIndex = rows.length;
138
+ if (antigravityAllowed) {
146
139
  rows.push(
147
140
  getProviderRow(
148
141
  currentTier,
149
- antigravityReady ? "antigravity" : "gemini",
150
- antigravityReady ? "a" : "g",
142
+ "antigravity",
143
+ "a",
151
144
  geminiBlue,
152
145
  accountsConfig,
153
146
  accountsState,
154
- antigravityReady
155
- ? {
156
- currentAbbrev: getAntigravityModelAbbrev(
157
- getAntigravityCurrentModel(),
158
- ),
159
- }
160
- : null,
161
- antigravityReady ? getAntigravityAccountLabel() : geminiEmail,
147
+ antigravityQuota,
148
+ getAntigravityAccountLabel(),
162
149
  ),
163
150
  );
164
151
  }
165
- const teamRow = getTeamRow(currentTier);
166
- if (teamRow) rows.push(teamRow);
167
152
 
168
153
  const outputLines = renderAlignedRows(rows);
169
- if (!codexBuckets && outputLines[codexRowIndex] != null) {
154
+ // 회색은 로그인 안 된 경우에만 쓴다. 로그인 상태의 조회 공백은 --% 로만 보인다.
155
+ if (
156
+ outputLines[codexRowIndex] != null &&
157
+ !codexBuckets &&
158
+ !existsSync(CODEX_AUTH_PATH) &&
159
+ !hasBrokerCodexAccounts()
160
+ ) {
170
161
  outputLines[codexRowIndex] = `${DIM}${outputLines[codexRowIndex]}${RESET}`;
171
162
  }
172
- if (!antigravityReady && outputLines[geminiRowIndex] != null) {
173
- outputLines[geminiRowIndex] = dim(outputLines[geminiRowIndex]);
174
- }
175
163
  // 알림 배너와 TUI 스타일이 HUD 내용에 겹치지 않도록 한다.
176
164
  const leadingBreaks = contextView.percent >= 85 ? "\n\n" : "\n";
177
165
  const resetLines = outputLines.map((line) => `\x1b[0m${line}`);
@@ -180,7 +168,7 @@ async function main() {
180
168
 
181
169
  main().catch(() => {
182
170
  process.stdout.write(
183
- `\x1b[0m${bold(claudeOrange("c"))}: ${dim("5h:--% (n/a) 1w:--% (n/a) | ctx:--%")}\n`,
171
+ `\x1b[0m${bold(claudeOrange("c"))}: ${dim(`5h:--% ${formatTimeCell("")} 1w:--% ${formatTimeCellDH("")} | CTX:--%`)}\n`,
184
172
  );
185
173
  });
186
174
 
@@ -1,10 +1,16 @@
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
1
3
  import {
2
- ANTIGRAVITY_MODEL_ABBREV,
3
4
  ANTIGRAVITY_OAUTH_PATHS,
5
+ ANTIGRAVITY_QUOTA_CACHE_PATH,
6
+ ANTIGRAVITY_QUOTA_STALE_MS,
7
+ ANTIGRAVITY_REFRESH_FLAG,
8
+ ANTIGRAVITY_REFRESH_LOCK_PATH,
4
9
  ANTIGRAVITY_SETTINGS_PATH,
5
- GEMINI_OAUTH_PATH,
10
+ ANTIGRAVITY_SIGNED_IN_GRACE_MS,
11
+ SPAWN_LOCK_TTL_MS,
6
12
  } from "../constants.mjs";
7
- import { decodeJwtEmail, readJson } from "../utils.mjs";
13
+ import { decodeJwtEmail, readJson, writeJsonSafe } from "../utils.mjs";
8
14
 
9
15
  function getCredentialEmail(credential) {
10
16
  return (
@@ -17,10 +23,6 @@ function getCredentialEmail(credential) {
17
23
  );
18
24
  }
19
25
 
20
- export function getGeminiEmail() {
21
- return getCredentialEmail(readJson(GEMINI_OAUTH_PATH, null));
22
- }
23
-
24
26
  // GCP 프로젝트 인증이면 계정 이메일 대신 프로젝트 ID를 보여 준다.
25
27
  export function getAntigravityAccountLabel() {
26
28
  const project = readJson(ANTIGRAVITY_SETTINGS_PATH, null)?.gcp?.project;
@@ -36,20 +38,123 @@ export function getAntigravityAccountLabel() {
36
38
  return null;
37
39
  }
38
40
 
39
- export function getAntigravityCurrentModel() {
40
- const settings = readJson(ANTIGRAVITY_SETTINGS_PATH, null);
41
- return settings?.model || null;
41
+ function isGcpProjectAuth() {
42
+ return Boolean(readJson(ANTIGRAVITY_SETTINGS_PATH, null)?.gcp?.project);
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
+
57
+ export function readAntigravityQuotaSnapshot() {
58
+ // 프로젝트 과금의 기본 응답을 개인 플랜의 0% 사용률로 표시하지 않는다.
59
+ if (isGcpProjectAuth()) return { data: null, shouldRefresh: false };
60
+ const cache = readJson(ANTIGRAVITY_QUOTA_CACHE_PATH, null);
61
+ const matched = cache?.accountLabel === getAntigravityAccountLabel();
62
+ const stale =
63
+ !cache?.timestamp ||
64
+ Date.now() - cache.timestamp >= ANTIGRAVITY_QUOTA_STALE_MS;
65
+ const model = readJson(ANTIGRAVITY_SETTINGS_PATH, null)?.model;
66
+ const buckets = matched && Array.isArray(cache?.buckets) ? cache.buckets : [];
67
+ const bucket = buckets
68
+ .filter(
69
+ (item) =>
70
+ model &&
71
+ item &&
72
+ (item?.name === model || item?.id === model) &&
73
+ Number.isFinite(item.remaining_fraction) &&
74
+ item.remaining_fraction >= 0 &&
75
+ item.remaining_fraction <= 1 &&
76
+ Date.parse(item.reset_time) > Date.now(),
77
+ )
78
+ .sort((a, b) => a.remaining_fraction - b.remaining_fraction)[0];
79
+ return {
80
+ data: bucket
81
+ ? {
82
+ usedPercent: Math.round((1 - bucket.remaining_fraction) * 100),
83
+ resetTime: bucket.reset_time,
84
+ stale,
85
+ }
86
+ : null,
87
+ shouldRefresh: !matched || stale,
88
+ };
89
+ }
90
+
91
+ // /usage는 모델 턴을 시작하지 않는 공식 print-mode 조회다.
92
+ export function refreshAntigravityQuotaCache(execFileSyncFn = execFileSync) {
93
+ if (isGcpProjectAuth()) return null;
94
+ const accountLabel = getAntigravityAccountLabel();
95
+ let buckets = null;
96
+ try {
97
+ const result = JSON.parse(
98
+ execFileSyncFn("agy", ["-p", "/usage", "--output-format", "json"], {
99
+ encoding: "utf8",
100
+ timeout: 15_000,
101
+ maxBuffer: 1024 * 1024,
102
+ stdio: ["ignore", "pipe", "ignore"],
103
+ windowsHide: true,
104
+ }),
105
+ );
106
+ if (
107
+ result.status === "SUCCESS" &&
108
+ result.command?.name === "usage" &&
109
+ Array.isArray(result.command.data?.groups)
110
+ ) {
111
+ buckets = result.command.data.groups.flatMap((group) =>
112
+ Array.isArray(group?.buckets) ? group.buckets : [],
113
+ );
114
+ }
115
+ } catch {
116
+ // 실패도 캐시해 인증 오류나 CLI 부재 시 매 렌더마다 실행하지 않는다.
117
+ }
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;
125
+ writeJsonSafe(ANTIGRAVITY_QUOTA_CACHE_PATH, {
126
+ timestamp: Date.now(),
127
+ accountLabel,
128
+ buckets,
129
+ signedInAt,
130
+ });
131
+ }
132
+ return buckets;
42
133
  }
43
134
 
44
- export function getAntigravityModelAbbrev(label) {
45
- if (!label) return "??";
46
- const known = ANTIGRAVITY_MODEL_ABBREV[label];
47
- if (known) return known;
48
- const parens = label.match(/\(([^)]+)\)/);
49
- const head = label.match(/^(\w+)/);
50
- if (head && parens) {
51
- return `${head[1][0].toUpperCase()}${parens[1][0].toLowerCase()}`;
135
+ export function scheduleAntigravityQuotaRefresh() {
136
+ const scriptPath = process.argv[1];
137
+ if (
138
+ !scriptPath ||
139
+ !existsSync(ANTIGRAVITY_SETTINGS_PATH) ||
140
+ isGcpProjectAuth()
141
+ )
142
+ return;
143
+ const lock = readJson(ANTIGRAVITY_REFRESH_LOCK_PATH, null);
144
+ if (Date.now() - Number(lock?.t || 0) < SPAWN_LOCK_TTL_MS) return;
145
+ writeJsonSafe(ANTIGRAVITY_REFRESH_LOCK_PATH, { t: Date.now() });
146
+ try {
147
+ const child = spawn(
148
+ process.execPath,
149
+ [scriptPath, ANTIGRAVITY_REFRESH_FLAG],
150
+ {
151
+ detached: true,
152
+ stdio: "ignore",
153
+ windowsHide: true,
154
+ },
155
+ );
156
+ child.unref();
157
+ } catch {
158
+ // 다음 렌더에서 다시 시도한다.
52
159
  }
53
- if (head) return head[1].slice(0, 2);
54
- return "??";
55
160
  }