@triflux/core 10.52.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.
@@ -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;
package/hud/renderers.mjs CHANGED
@@ -85,7 +85,11 @@ export function getMicroLine(
85
85
  codexBuckets,
86
86
  options = {},
87
87
  ) {
88
- const { showCodex = true, showAntigravity = true } = options;
88
+ const {
89
+ showCodex = true,
90
+ showAntigravity = true,
91
+ codexLoggedOut = false,
92
+ } = options;
89
93
  const ctxView = contextView || buildContextUsageView({});
90
94
  // Claude 5h/1w
91
95
  const cF =
@@ -98,11 +102,11 @@ export function getMicroLine(
98
102
  : null;
99
103
  const cVal =
100
104
  claudeUsage != null
101
- ? `${cF != null ? colorByProvider(cF, `${cF}`, claudeOrange) : dim("--")}${dim("/")}${cW != null ? colorByProvider(cW, `${cW}`, claudeOrange) : dim("--")}`
102
- : dim("--/--");
105
+ ? `${cF != null ? colorByProvider(cF, `${cF}%`, claudeOrange) : dim("--%")}${dim("/")}${cW != null ? colorByProvider(cW, `${cW}%`, claudeOrange) : dim("--%")}`
106
+ : dim("--%/--%");
103
107
 
104
108
  // Codex 5h/1w
105
- let xVal = dim("--/--");
109
+ let xVal = dim("--%/--%");
106
110
  if (codexBuckets) {
107
111
  const mb = codexBuckets.codex || codexBuckets[Object.keys(codexBuckets)[0]];
108
112
  if (mb) {
@@ -114,7 +118,7 @@ export function getMicroLine(
114
118
  mb.secondary?.used_percent != null
115
119
  ? clampPercent(mb.secondary.used_percent)
116
120
  : null;
117
- xVal = `${xF != null ? colorByProvider(xF, `${xF}`, codexWhite) : dim("--")}${dim("/")}${xW != null ? colorByProvider(xW, `${xW}`, codexWhite) : dim("--")}`;
121
+ xVal = `${xF != null ? colorByProvider(xF, `${xF}%`, codexWhite) : dim("--%")}${dim("/")}${xW != null ? colorByProvider(xW, `${xW}%`, codexWhite) : dim("--%")}`;
118
122
  }
119
123
  }
120
124
 
@@ -122,15 +126,19 @@ export function getMicroLine(
122
126
  // 세그먼트를 모아 join 한다. 차단된 프로바이더를 뺄 때 공백이 겹치지 않는다.
123
127
  const segments = [`${bold(claudeOrange("c"))}${dim(":")}${cVal}`];
124
128
  if (showCodex) {
125
- segments.push(`${bold(codexWhite("x"))}${dim(":")}${xVal}`);
129
+ const xMarker = codexLoggedOut ? dim("x") : bold(codexWhite("x"));
130
+ segments.push(`${xMarker}${dim(":")}${xVal}`);
126
131
  }
127
132
  if (showAntigravity) {
128
133
  const marker = options.antigravityQuota?.auth
129
134
  ? bold(geminiBlue("a"))
130
135
  : dim("a");
131
- segments.push(
132
- `${marker}${dim(":")}${antigravityPercentText(options.antigravityQuota)}${options.antigravityQuota?.stale ? dim("*") : ""}`,
133
- );
136
+ // stale 은 값을 dim 으로 보인다. 표식을 덧붙이면 최악 폭이 39칸을 넘는다.
137
+ const agyText = antigravityPercentText(options.antigravityQuota);
138
+ const agyValue = options.antigravityQuota?.stale
139
+ ? dim(stripAnsi(agyText))
140
+ : agyText;
141
+ segments.push(`${marker}${dim(":")}${agyValue}`);
134
142
  }
135
143
  segments.push(`${dim("CTX:")}${contextPercentText(ctxView)}`);
136
144
  return truncateAnsi(segments.join(" "), cols);
@@ -243,24 +251,9 @@ export function getClaudeRows(currentTier, contextView, claudeUsage) {
243
251
  // ============================================================================
244
252
  // 계정 라벨 + 범용 프로바이더 행 렌더러
245
253
  // ============================================================================
246
- export function getAccountLabel(
247
- provider,
248
- accountsConfig,
249
- accountsState,
250
- codexEmail,
251
- ) {
252
- const providerConfig = accountsConfig?.providers?.[provider] || [];
253
- const providerState = accountsState?.providers?.[provider] || {};
254
- const lastId = providerState.last_selected_id;
255
- const picked = providerConfig.find((a) => a.id === lastId) ||
256
- providerConfig[0] || {
257
- id: `${provider}-main`,
258
- label: provider === "antigravity" ? "agy" : provider,
259
- };
260
- let label = picked.label || picked.id;
261
- if (codexEmail) label = codexEmail;
262
- if (label.includes("@")) label = label.split("@")[0];
263
- return label;
254
+ export function getAccountLabel(provider, accountName) {
255
+ const label = accountName || (provider === "antigravity" ? "agy" : provider);
256
+ return label.includes("@") ? label.split("@")[0] : label;
264
257
  }
265
258
 
266
259
  export function getProviderRow(
@@ -268,13 +261,11 @@ export function getProviderRow(
268
261
  provider,
269
262
  marker,
270
263
  markerColor,
271
- accountsConfig,
272
- accountsState,
273
264
  realQuota,
274
- codexEmail,
265
+ accountName,
275
266
  ) {
276
267
  const accountLabel = fitText(
277
- getAccountLabel(provider, accountsConfig, accountsState, codexEmail),
268
+ getAccountLabel(provider, accountName),
278
269
  ACCOUNT_LABEL_WIDTH,
279
270
  );
280
271
 
package/hud/utils.mjs CHANGED
@@ -254,14 +254,6 @@ export function formatResetRemainingDayHour(isoOrUnix, cycleMs = 0) {
254
254
  return `${String(days).padStart(2, "0")}d${String(hours).padStart(2, "0")}h`;
255
255
  }
256
256
 
257
- export function getProviderAccountId(provider, accountsConfig, accountsState) {
258
- const providerState = accountsState?.providers?.[provider] || {};
259
- const selectedId = providerState.last_selected_id;
260
- if (selectedId) return selectedId;
261
- const providerConfig = accountsConfig?.providers?.[provider] || [];
262
- return providerConfig[0]?.id || `${provider}-main`;
263
- }
264
-
265
257
  // JWT base64 디코딩 공통 헬퍼
266
258
  export function decodeJwtEmail(idToken) {
267
259
  if (!idToken) return null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@triflux/core",
3
- "version": "10.52.0",
4
- "description": "triflux core — CLI routing, adapters, account broker. Zero native dependencies.",
3
+ "version": "10.53.0",
4
+ "description": "triflux core — CLI routing, adapters. Zero native dependencies.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
7
7
  "exports": {
@@ -13,8 +13,7 @@
13
13
  "node": ">=20"
14
14
  },
15
15
  "dependencies": {
16
- "@iarna/toml": "^2.2.5",
17
- "zod": "^4.0.0"
16
+ "@iarna/toml": "^2.2.5"
18
17
  },
19
18
  "files": [
20
19
  "hub",
@@ -40,7 +40,6 @@ const DEFAULT_REGISTRY = Object.freeze({
40
40
  unknown_server_action: "warn",
41
41
  sync_denylist: [],
42
42
  watched_paths: [
43
- "~/.gemini/settings.json",
44
43
  "~/.codex/config.toml",
45
44
  "~/.claude.json",
46
45
  "~/.claude/settings.json",
@@ -709,7 +708,7 @@ function serverTargets(serverConfig) {
709
708
  ),
710
709
  ];
711
710
  }
712
- return ["claude", "gemini", "codex", "antigravity"];
711
+ return ["claude", "codex", "antigravity"];
713
712
  }
714
713
 
715
714
  function serverAppliesToClient(serverConfig, client) {
@@ -1718,7 +1717,7 @@ export function addRegistryServer(name, url, options = {}) {
1718
1717
  .filter(Boolean),
1719
1718
  ),
1720
1719
  ]
1721
- : ["claude", "gemini", "codex", "antigravity"],
1720
+ : ["claude", "codex", "antigravity"],
1722
1721
  description: options.description || `${trimmedName} MCP 서버`,
1723
1722
  };
1724
1723