@triflux/core 10.50.2 → 10.51.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 (46) 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 +1 -79
  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 +1 -1
  8. package/hub/lib/known-errors.json +2 -2
  9. package/hub/lib/process-utils.mjs +0 -123
  10. package/hub/lib/spawn-trace.mjs +3 -2
  11. package/hub/lib/timeout-defaults.mjs +0 -1
  12. package/hub/role-contract.mjs +0 -2
  13. package/hud/cli-policy.mjs +0 -22
  14. package/hud/constants.mjs +11 -26
  15. package/hud/hud-qos-status.mjs +23 -51
  16. package/hud/providers/gemini.mjs +105 -20
  17. package/hud/renderers.mjs +50 -104
  18. package/hud/utils.mjs +2 -2
  19. package/package.json +1 -3
  20. package/scripts/lib/agent-route-policy.mjs +162 -14
  21. package/scripts/lib/claudemd-scanner.mjs +2 -199
  22. package/scripts/lib/cli-claude.mjs +0 -2
  23. package/scripts/lib/context.mjs +3 -19
  24. package/scripts/lib/env-probe.mjs +5 -64
  25. package/scripts/lib/gemini-profiles.mjs +2 -69
  26. package/scripts/lib/machine-profile.mjs +3 -17
  27. package/scripts/lib/mcp-filter.mjs +109 -53
  28. package/scripts/lib/mcp-health.mjs +2 -2
  29. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  30. package/scripts/lib/psmux-info.mjs +11 -59
  31. package/scripts/lib/session-presence.mjs +1 -1
  32. package/scripts/lib/skill-template.mjs +0 -245
  33. package/hub/dynamic-routing-engine.mjs +0 -511
  34. package/hub/lib/risk-tier.mjs +0 -53
  35. package/hub/lib/ssh-command.mjs +0 -231
  36. package/hub/lib/staged-file-detect.mjs +0 -43
  37. package/hub/lib/tfx-route-args.mjs +0 -353
  38. package/hub/routing-snapshot.mjs +0 -261
  39. package/mesh/index.mjs +0 -69
  40. package/mesh/mesh-budget.mjs +0 -137
  41. package/mesh/mesh-heartbeat.mjs +0 -100
  42. package/mesh/mesh-protocol.mjs +0 -96
  43. package/mesh/mesh-queue.mjs +0 -163
  44. package/mesh/mesh-registry.mjs +0 -78
  45. package/mesh/mesh-router.mjs +0 -77
  46. package/scripts/lib/dynamic-route-cli.mjs +0 -107
@@ -488,68 +488,6 @@ function hasExactGbrainServe(commandLine) {
488
488
  );
489
489
  }
490
490
 
491
- function hasGbrainServeCommand(commandLine) {
492
- return /(^|\s)gbrain\s+serve(\s|$)/i.test(commandLine.trim());
493
- }
494
-
495
- function hasFsmonitorDaemonCommand(commandLine) {
496
- return FSMONITOR_DAEMON_PATTERN.test(commandLine);
497
- }
498
-
499
- function categoryForShardProcess(proc, context) {
500
- const name = normalizeName(proc.name);
501
- const commandLine = normalizeCommandLine(proc.commandLine);
502
- const worktreePath = normalizeCommandLine(context.worktreePath || "");
503
- const shardMarker = context.shardName
504
- ? `.codex-swarm\\wt-${context.shardName}`
505
- : "";
506
- const hasWorktree = worktreePath && commandLine.includes(worktreePath);
507
- const hasShardMarker = shardMarker && commandLine.includes(shardMarker);
508
-
509
- if (name === "node.exe" || name === "node") {
510
- if (hasWorktree || hasShardMarker) return "node";
511
- }
512
-
513
- if (name === "bash.exe" || name === "bash" || name === "sh") {
514
- if (hasWorktree) return "bash";
515
- }
516
-
517
- if (name === "conhost.exe" || name === "conhost") {
518
- if (
519
- context.sessionIds?.length > 0 &&
520
- context.sessionIds.some((id) => id && commandLine.includes(id))
521
- ) {
522
- return "conhost";
523
- }
524
- }
525
-
526
- if (name === "bun.exe" || name === "bun") {
527
- if ((hasWorktree || hasShardMarker) && hasGbrainServeCommand(commandLine)) {
528
- return "bun";
529
- }
530
- }
531
-
532
- if (name === "git.exe" || name === "git") {
533
- if (
534
- (hasWorktree || hasShardMarker) &&
535
- hasFsmonitorDaemonCommand(commandLine)
536
- ) {
537
- return "git";
538
- }
539
- }
540
-
541
- return null;
542
- }
543
-
544
- function killPid(
545
- pid,
546
- { killFn = process.kill, protectedPids = new Set() } = {},
547
- ) {
548
- if (protectedPids.has(pid)) return false;
549
- killFn(pid, "SIGKILL");
550
- return true;
551
- }
552
-
553
491
  /**
554
492
  * Return a process tree rooted at `rootPid`.
555
493
  *
@@ -730,67 +668,6 @@ export function findProcessesByCommandLine(
730
668
  });
731
669
  }
732
670
 
733
- /**
734
- * Cleanup processes associated with a swarm shard.
735
- *
736
- * Sequence: kill known top PID snapshots, scan scoped command lines, skip
737
- * protected PIDs, and kill only narrowly gated runtime categories.
738
- *
739
- * @param {{worktreePath?: string, sessionIds?: string[], topPids?: Array<number | {pid: number}>, runId?: string, shardName?: string, dryRun?: boolean, isWindows?: boolean, spawnSyncFn?: typeof spawnSync, killFn?: typeof process.kill, protectedPids?: Set<number>}} opts
740
- * @returns {{killed: number, scanned: number, skipped: number, byCategory: {node: number, bash: number, conhost: number, bun: number, git: number}}}
741
- */
742
- export function cleanupShardProcesses({
743
- worktreePath,
744
- sessionIds = [],
745
- topPids = [],
746
- runId,
747
- shardName,
748
- dryRun = false,
749
- isWindows = IS_WINDOWS,
750
- spawnSyncFn = spawnSync,
751
- killFn = process.kill,
752
- protectedPids,
753
- } = {}) {
754
- const protectedSet = getProtectedPids({
755
- protectedPids,
756
- isWindows,
757
- spawnSyncFn,
758
- includeAncestorScan: true,
759
- });
760
- const byCategory = { node: 0, bash: 0, conhost: 0, bun: 0, git: 0 };
761
- let killed = 0;
762
- let skipped = 0;
763
-
764
- if (!dryRun && topPids.length > 0) {
765
- killed += killProcessTreeSnapshot(topPids, {
766
- killFn,
767
- protectedPids: protectedSet,
768
- }).killed;
769
- }
770
-
771
- const scanned = getProcessSnapshot({ isWindows, spawnSyncFn });
772
-
773
- for (const proc of scanned) {
774
- const category = categoryForShardProcess(proc, {
775
- worktreePath,
776
- sessionIds,
777
- shardName,
778
- });
779
- if (!category) continue;
780
- byCategory[category]++;
781
- if (protectedSet.has(proc.pid)) {
782
- skipped++;
783
- continue;
784
- }
785
- if (dryRun) continue;
786
- try {
787
- if (killPid(proc.pid, { killFn, protectedPids: protectedSet })) killed++;
788
- } catch {}
789
- }
790
-
791
- return { killed, scanned: scanned.length, skipped, byCategory };
792
- }
793
-
794
671
  /**
795
672
  * Cleanup narrowly gated orphan runtime processes.
796
673
  *
@@ -7,13 +7,14 @@ import { join } from "node:path";
7
7
  const LOG_DIR = join(homedir(), ".triflux", "logs");
8
8
  const DEDUPE_WINDOW_MS = 5_000;
9
9
  const RATE_WINDOW_MS = 1_000;
10
+ const DEFAULT_MAX_SPAWN_PER_SEC = 100;
10
11
  // multi-worker headless dispatch 가 1 초 안에 ~25-30+ psmux/tmux 명령을 호출한다.
11
12
  // 2-worker dispatchBatch 가 default 30 limit 을 초과해 `rate_limit` throw 로 mac
12
13
  // 호환성 회귀 (smoke test 발견, 2026-05-15). 100 으로 상향 — 폭주 안전망은
13
14
  // 여전히 (default 100/sec) 유지, env override 로 보수 설정 가능.
14
15
  export let MAX_SPAWN_PER_SEC = resolvePositiveInteger(
15
16
  process.env.TRIFLUX_MAX_SPAWN_RATE,
16
- 100,
17
+ DEFAULT_MAX_SPAWN_PER_SEC,
17
18
  );
18
19
  export const MAX_TOTAL_DESCENDANTS = resolvePositiveInteger(
19
20
  process.env.TRIFLUX_MAX_DESCENDANTS,
@@ -160,7 +161,7 @@ export function getMaxSpawnPerSec() {
160
161
  export function reload() {
161
162
  MAX_SPAWN_PER_SEC = resolvePositiveInteger(
162
163
  process.env.TRIFLUX_MAX_SPAWN_RATE,
163
- 30,
164
+ DEFAULT_MAX_SPAWN_PER_SEC,
164
165
  );
165
166
  return getMaxSpawnPerSec();
166
167
  }
@@ -5,7 +5,6 @@ export const MAX_DURATION_MS = 86_400_000;
5
5
  export const DEFAULT_ASSIGN_TIMEOUT_MS = 600_000;
6
6
  export const DEFAULT_ASSIGN_TTL_MS = 600_000;
7
7
  export const DEFAULT_HANDOFF_TTL_MS = 600_000;
8
- export const DEFAULT_SWARM_LOCK_TTL_MS = 600_000;
9
8
  export const DEFAULT_REGISTER_TIMEOUT_SEC = 600;
10
9
  export const REGISTER_HEARTBEAT_GRACE_MS = 120_000;
11
10
  export const DEFAULT_STALL_INTERVENTION_SEC = 1_200;
@@ -216,8 +216,6 @@ export function reduceRoleReachabilityTransition(state, trigger) {
216
216
  : { state: transition.to, action: transition.action };
217
217
  }
218
218
 
219
- export const reduceRoleReachability = reduceRoleReachabilityTransition;
220
-
221
219
  function snapshotLegacyDetails(snapshot) {
222
220
  const legacy = isRecord(snapshot.legacy) ? snapshot.legacy : {};
223
221
  return {
@@ -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,23 @@ 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
+ export const ANTIGRAVITY_REFRESH_FLAG = "--refresh-antigravity-quota";
190
175
  export const ACCOUNT_LABEL_WIDTH = 10;
191
176
  export const PROVIDER_PREFIX_WIDTH = 2;
192
- export const PERCENT_CELL_WIDTH = 3;
177
+ export const PERCENT_CELL_WIDTH = 4;
193
178
  export const TIME_CELL_INNER_WIDTH = 6;
194
179
 
195
180
  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,11 @@ 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,
25
23
  CODEX_REFRESH_FLAG,
26
- TFX_PREFLIGHT_CACHE_PATH,
27
- TFX_PREFLIGHT_CACHE_STALE_MS,
28
24
  } from "./constants.mjs";
29
25
  import { buildContextUsageView } from "./context-monitor.mjs";
30
26
  import {
@@ -41,15 +37,14 @@ import {
41
37
  } from "./providers/codex.mjs";
42
38
  import {
43
39
  getAntigravityAccountLabel,
44
- getAntigravityCurrentModel,
45
- getAntigravityModelAbbrev,
46
- getGeminiEmail,
40
+ readAntigravityQuotaSnapshot,
41
+ refreshAntigravityQuotaCache,
42
+ scheduleAntigravityQuotaRefresh,
47
43
  } from "./providers/gemini.mjs";
48
44
  import {
49
45
  getClaudeRows,
50
46
  getMicroLine,
51
47
  getProviderRow,
52
- getTeamRow,
53
48
  renderAlignedRows,
54
49
  } from "./renderers.mjs";
55
50
  import { selectTier } from "./terminal.mjs";
@@ -64,22 +59,21 @@ async function main() {
64
59
  await refreshCodexRateLimitsCache();
65
60
  return;
66
61
  }
62
+ if (process.argv.includes(ANTIGRAVITY_REFRESH_FLAG)) {
63
+ refreshAntigravityQuotaCache();
64
+ return;
65
+ }
67
66
 
68
67
  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
68
  const { showCodex, antigravityAllowed } = resolveHudCliVisibility();
75
- const antigravityReady =
76
- antigravityAllowed &&
77
- preflightFresh &&
78
- preflightCache?.antigravity?.ok === true;
79
69
  const accountsConfig = readJson(ACCOUNTS_CONFIG_PATH, { providers: {} });
80
70
  const accountsState = readJson(ACCOUNTS_STATE_PATH, { providers: {} });
81
71
  const claudeUsageSnapshot = readClaudeUsageSnapshot();
82
72
  const codexSnapshot = readCodexRateLimitSnapshot();
73
+ const antigravitySnapshot = antigravityAllowed
74
+ ? readAntigravityQuotaSnapshot()
75
+ : null;
76
+ if (antigravitySnapshot?.shouldRefresh) scheduleAntigravityQuotaRefresh();
83
77
  // 설정이 없는 홈에서는 갱신 프로세스를 시작하지 않는다.
84
78
  if (
85
79
  claudeUsageSnapshot.shouldRefresh &&
@@ -102,21 +96,12 @@ async function main() {
102
96
  : null;
103
97
  const codexBuckets = codexSnapshot.buckets;
104
98
  const currentTier = selectTier();
105
- const geminiEmail = getGeminiEmail();
106
- const showGeminiRow = shouldRenderGeminiFallbackRow({
107
- antigravityAllowed,
108
- antigravityReady,
109
- geminiEmail,
110
- });
111
-
112
99
  if (currentTier === "nano") {
113
- const microLine = getMicroLine(
114
- contextView,
115
- claudeUsage,
116
- codexBuckets,
117
- antigravityReady,
118
- { showCodex, showGemini: showGeminiRow },
119
- );
100
+ const microLine = getMicroLine(contextView, claudeUsage, codexBuckets, {
101
+ showCodex,
102
+ showAntigravity: antigravityAllowed,
103
+ antigravityQuota: antigravitySnapshot?.data,
104
+ });
120
105
  process.stdout.write(`\x1b[0m${microLine}\n`);
121
106
  return;
122
107
  }
@@ -140,38 +125,25 @@ async function main() {
140
125
  ),
141
126
  );
142
127
  }
143
- let geminiRowIndex = -1;
144
- if (showGeminiRow) {
145
- geminiRowIndex = rows.length;
128
+ if (antigravityAllowed) {
146
129
  rows.push(
147
130
  getProviderRow(
148
131
  currentTier,
149
- antigravityReady ? "antigravity" : "gemini",
150
- antigravityReady ? "a" : "g",
132
+ "antigravity",
133
+ "a",
151
134
  geminiBlue,
152
135
  accountsConfig,
153
136
  accountsState,
154
- antigravityReady
155
- ? {
156
- currentAbbrev: getAntigravityModelAbbrev(
157
- getAntigravityCurrentModel(),
158
- ),
159
- }
160
- : null,
161
- antigravityReady ? getAntigravityAccountLabel() : geminiEmail,
137
+ antigravitySnapshot?.data,
138
+ getAntigravityAccountLabel(),
162
139
  ),
163
140
  );
164
141
  }
165
- const teamRow = getTeamRow(currentTier);
166
- if (teamRow) rows.push(teamRow);
167
142
 
168
143
  const outputLines = renderAlignedRows(rows);
169
144
  if (!codexBuckets && outputLines[codexRowIndex] != null) {
170
145
  outputLines[codexRowIndex] = `${DIM}${outputLines[codexRowIndex]}${RESET}`;
171
146
  }
172
- if (!antigravityReady && outputLines[geminiRowIndex] != null) {
173
- outputLines[geminiRowIndex] = dim(outputLines[geminiRowIndex]);
174
- }
175
147
  // 알림 배너와 TUI 스타일이 HUD 내용에 겹치지 않도록 한다.
176
148
  const leadingBreaks = contextView.percent >= 85 ? "\n\n" : "\n";
177
149
  const resetLines = outputLines.map((line) => `\x1b[0m${line}`);
@@ -1,10 +1,15 @@
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
+ SPAWN_LOCK_TTL_MS,
6
11
  } from "../constants.mjs";
7
- import { decodeJwtEmail, readJson } from "../utils.mjs";
12
+ import { decodeJwtEmail, readJson, writeJsonSafe } from "../utils.mjs";
8
13
 
9
14
  function getCredentialEmail(credential) {
10
15
  return (
@@ -17,10 +22,6 @@ function getCredentialEmail(credential) {
17
22
  );
18
23
  }
19
24
 
20
- export function getGeminiEmail() {
21
- return getCredentialEmail(readJson(GEMINI_OAUTH_PATH, null));
22
- }
23
-
24
25
  // GCP 프로젝트 인증이면 계정 이메일 대신 프로젝트 ID를 보여 준다.
25
26
  export function getAntigravityAccountLabel() {
26
27
  const project = readJson(ANTIGRAVITY_SETTINGS_PATH, null)?.gcp?.project;
@@ -36,20 +37,104 @@ export function getAntigravityAccountLabel() {
36
37
  return null;
37
38
  }
38
39
 
39
- export function getAntigravityCurrentModel() {
40
- const settings = readJson(ANTIGRAVITY_SETTINGS_PATH, null);
41
- return settings?.model || null;
40
+ function isGcpProjectAuth() {
41
+ return Boolean(readJson(ANTIGRAVITY_SETTINGS_PATH, null)?.gcp?.project);
42
+ }
43
+
44
+ export function readAntigravityQuotaSnapshot() {
45
+ // 프로젝트 과금의 기본 응답을 개인 플랜의 0% 사용률로 표시하지 않는다.
46
+ if (isGcpProjectAuth()) return { data: null, shouldRefresh: false };
47
+ const cache = readJson(ANTIGRAVITY_QUOTA_CACHE_PATH, null);
48
+ const matched = cache?.accountLabel === getAntigravityAccountLabel();
49
+ const stale =
50
+ !cache?.timestamp ||
51
+ Date.now() - cache.timestamp >= ANTIGRAVITY_QUOTA_STALE_MS;
52
+ const model = readJson(ANTIGRAVITY_SETTINGS_PATH, null)?.model;
53
+ const buckets = matched && Array.isArray(cache?.buckets) ? cache.buckets : [];
54
+ const bucket = buckets
55
+ .filter(
56
+ (item) =>
57
+ model &&
58
+ item &&
59
+ (item?.name === model || item?.id === model) &&
60
+ Number.isFinite(item.remaining_fraction) &&
61
+ item.remaining_fraction >= 0 &&
62
+ item.remaining_fraction <= 1 &&
63
+ Date.parse(item.reset_time) > Date.now(),
64
+ )
65
+ .sort((a, b) => a.remaining_fraction - b.remaining_fraction)[0];
66
+ return {
67
+ data: bucket
68
+ ? {
69
+ usedPercent: Math.round((1 - bucket.remaining_fraction) * 100),
70
+ resetTime: bucket.reset_time,
71
+ stale,
72
+ }
73
+ : null,
74
+ shouldRefresh: !matched || stale,
75
+ };
76
+ }
77
+
78
+ // /usage는 모델 턴을 시작하지 않는 공식 print-mode 조회다.
79
+ export function refreshAntigravityQuotaCache(execFileSyncFn = execFileSync) {
80
+ if (isGcpProjectAuth()) return null;
81
+ const accountLabel = getAntigravityAccountLabel();
82
+ let buckets = null;
83
+ try {
84
+ const result = JSON.parse(
85
+ execFileSyncFn("agy", ["-p", "/usage", "--output-format", "json"], {
86
+ encoding: "utf8",
87
+ timeout: 15_000,
88
+ maxBuffer: 1024 * 1024,
89
+ stdio: ["ignore", "pipe", "ignore"],
90
+ windowsHide: true,
91
+ }),
92
+ );
93
+ if (
94
+ result.status === "SUCCESS" &&
95
+ result.command?.name === "usage" &&
96
+ Array.isArray(result.command.data?.groups)
97
+ ) {
98
+ buckets = result.command.data.groups.flatMap((group) =>
99
+ Array.isArray(group?.buckets) ? group.buckets : [],
100
+ );
101
+ }
102
+ } catch {
103
+ // 실패도 캐시해 인증 오류나 CLI 부재 시 매 렌더마다 실행하지 않는다.
104
+ }
105
+ if (accountLabel === getAntigravityAccountLabel()) {
106
+ writeJsonSafe(ANTIGRAVITY_QUOTA_CACHE_PATH, {
107
+ timestamp: Date.now(),
108
+ accountLabel,
109
+ buckets,
110
+ });
111
+ }
112
+ return buckets;
42
113
  }
43
114
 
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()}`;
115
+ export function scheduleAntigravityQuotaRefresh() {
116
+ const scriptPath = process.argv[1];
117
+ if (
118
+ !scriptPath ||
119
+ !existsSync(ANTIGRAVITY_SETTINGS_PATH) ||
120
+ isGcpProjectAuth()
121
+ )
122
+ return;
123
+ const lock = readJson(ANTIGRAVITY_REFRESH_LOCK_PATH, null);
124
+ if (Date.now() - Number(lock?.t || 0) < SPAWN_LOCK_TTL_MS) return;
125
+ writeJsonSafe(ANTIGRAVITY_REFRESH_LOCK_PATH, { t: Date.now() });
126
+ try {
127
+ const child = spawn(
128
+ process.execPath,
129
+ [scriptPath, ANTIGRAVITY_REFRESH_FLAG],
130
+ {
131
+ detached: true,
132
+ stdio: "ignore",
133
+ windowsHide: true,
134
+ },
135
+ );
136
+ child.unref();
137
+ } catch {
138
+ // 다음 렌더에서 다시 시도한다.
52
139
  }
53
- if (head) return head[1].slice(0, 2);
54
- return "??";
55
140
  }