triflux 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.
- package/CLAUDE.md +0 -16
- package/README.ko.md +2 -1
- package/README.md +3 -2
- package/adapters/codex/skills/tfx-live/SKILL.md +15 -3
- package/bin/tfx-live.mjs +185 -15
- package/bin/triflux.mjs +14 -15
- package/config/mcp-registry.json +5 -6
- package/hub/bridge.mjs +4 -25
- package/hub/cli-adapter-base.mjs +4 -104
- package/hub/codex-adapter.mjs +7 -116
- package/hub/team/claude-transcript.mjs +19 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -0
- package/hub/team/codex-queue.mjs +108 -1
- package/hub/team/headless.mjs +36 -8
- package/hub/team/results-index.mjs +92 -0
- package/hub/team/uds-orchestrator.mjs +18 -19
- package/hud/constants.mjs +7 -22
- package/hud/hud-qos-status.mjs +9 -28
- package/hud/providers/codex-probe.mjs +2 -31
- package/hud/providers/codex.mjs +16 -58
- package/hud/renderers.mjs +22 -31
- package/hud/utils.mjs +0 -8
- package/package.json +1 -1
- package/scripts/__tests__/mcp-guard-engine.test.mjs +3 -3
- package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +1 -2
- package/scripts/doctor-diagnose.mjs +8 -59
- package/scripts/lib/mcp-guard-engine.mjs +2 -3
- package/scripts/mcp-check.mjs +0 -22
- package/scripts/pack.mjs +1 -3
- package/scripts/setup.mjs +5 -5
- package/scripts/tfx-route.sh +1 -2
- package/skills/tfx-lead/SKILL.md +18 -1
- package/skills/tfx-live/SKILL.md +15 -3
- package/skills/tfx-setup/SKILL.md +2 -0
- package/hub/account-broker.mjs +0 -1221
|
@@ -15,12 +15,12 @@ function codexClient(socketPath, timeoutMs, clientFactory) {
|
|
|
15
15
|
return clientFactory({ socketPath, connectTimeoutMs: timeoutMs });
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
async function initializeCodexClient(client, timeoutMs) {
|
|
18
|
+
async function initializeCodexClient(client, timeoutMs, remainingMs) {
|
|
19
19
|
await client.connect();
|
|
20
20
|
await client.request(
|
|
21
21
|
"initialize",
|
|
22
22
|
{ clientInfo: { name: "triflux-tfx-live", version: "1.0.0" } },
|
|
23
|
-
timeoutMs,
|
|
23
|
+
remainingMs ? Math.max(1, remainingMs()) : timeoutMs,
|
|
24
24
|
);
|
|
25
25
|
client.notify("initialized", {});
|
|
26
26
|
}
|
|
@@ -211,7 +211,14 @@ async function normalizedCwd(cwd) {
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
|
|
214
|
+
// timeoutMs 는 RPC 하나의 상한이다. remainingMs 를 주면 조회 전체가 그 안에 끝나야 한다.
|
|
215
|
+
async function loadedCodexThreads(client, cwd, timeoutMs, remainingMs) {
|
|
216
|
+
const limit = () => {
|
|
217
|
+
if (!remainingMs) return timeoutMs;
|
|
218
|
+
const left = remainingMs();
|
|
219
|
+
if (left <= 0) throw new Error("codex thread listing timed out");
|
|
220
|
+
return left;
|
|
221
|
+
};
|
|
215
222
|
const wantedCwd = await normalizedCwd(cwd);
|
|
216
223
|
const threads = [];
|
|
217
224
|
let cursor;
|
|
@@ -219,10 +226,10 @@ async function loadedCodexThreads(client, cwd, timeoutMs) {
|
|
|
219
226
|
const result = await client.request(
|
|
220
227
|
"thread/loaded/list",
|
|
221
228
|
cursor ? { cursor } : {},
|
|
222
|
-
|
|
229
|
+
limit(),
|
|
223
230
|
);
|
|
224
231
|
for (const threadId of result?.data || []) {
|
|
225
|
-
const thread = await readCodexThread(client, threadId,
|
|
232
|
+
const thread = await readCodexThread(client, threadId, limit());
|
|
226
233
|
if (wantedCwd && (await normalizedCwd(thread.cwd)) !== wantedCwd)
|
|
227
234
|
continue;
|
|
228
235
|
threads.push({
|
|
@@ -241,23 +248,15 @@ async function loadedCodexThreads(client, cwd, timeoutMs) {
|
|
|
241
248
|
export async function listCodexAppServerThreads({
|
|
242
249
|
socketPath,
|
|
243
250
|
cwd,
|
|
251
|
+
timeoutMs = DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
244
252
|
clientFactory = (opts) => new JsonRpcWsUdsClient(opts),
|
|
245
253
|
} = {}) {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
clientFactory,
|
|
250
|
-
);
|
|
254
|
+
const deadline = Date.now() + timeoutMs;
|
|
255
|
+
const remainingMs = () => deadline - Date.now();
|
|
256
|
+
const client = codexClient(socketPath, timeoutMs, clientFactory);
|
|
251
257
|
try {
|
|
252
|
-
await initializeCodexClient(
|
|
253
|
-
|
|
254
|
-
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
255
|
-
);
|
|
256
|
-
return await loadedCodexThreads(
|
|
257
|
-
client,
|
|
258
|
-
cwd,
|
|
259
|
-
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
260
|
-
);
|
|
258
|
+
await initializeCodexClient(client, timeoutMs, remainingMs);
|
|
259
|
+
return await loadedCodexThreads(client, cwd, timeoutMs, remainingMs);
|
|
261
260
|
} finally {
|
|
262
261
|
client.close();
|
|
263
262
|
}
|
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
|
-
|
|
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(
|
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -14,14 +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
|
-
CODEX_AUTH_PATH,
|
|
24
21
|
CODEX_REFRESH_FLAG,
|
|
22
|
+
getCodexAuthPath,
|
|
23
|
+
getCodexHome,
|
|
25
24
|
} from "./constants.mjs";
|
|
26
25
|
import { buildContextUsageView } from "./context-monitor.mjs";
|
|
27
26
|
import {
|
|
@@ -31,7 +30,6 @@ import {
|
|
|
31
30
|
} from "./providers/claude.mjs";
|
|
32
31
|
import {
|
|
33
32
|
getCodexEmail,
|
|
34
|
-
hasBrokerCodexAccounts,
|
|
35
33
|
readCodexRateLimitSnapshot,
|
|
36
34
|
refreshCodexRateLimitsCache,
|
|
37
35
|
scheduleCodexRateLimitRefresh,
|
|
@@ -50,12 +48,7 @@ import {
|
|
|
50
48
|
renderAlignedRows,
|
|
51
49
|
} from "./renderers.mjs";
|
|
52
50
|
import { selectTier } from "./terminal.mjs";
|
|
53
|
-
import {
|
|
54
|
-
formatTimeCell,
|
|
55
|
-
formatTimeCellDH,
|
|
56
|
-
readJson,
|
|
57
|
-
readStdinJson,
|
|
58
|
-
} from "./utils.mjs";
|
|
51
|
+
import { formatTimeCell, formatTimeCellDH, readStdinJson } from "./utils.mjs";
|
|
59
52
|
|
|
60
53
|
async function main() {
|
|
61
54
|
if (process.argv.includes(CLAUDE_REFRESH_FLAG)) {
|
|
@@ -73,8 +66,6 @@ async function main() {
|
|
|
73
66
|
|
|
74
67
|
const stdinPromise = readStdinJson();
|
|
75
68
|
const { showCodex, antigravityAllowed } = resolveHudCliVisibility();
|
|
76
|
-
const accountsConfig = readJson(ACCOUNTS_CONFIG_PATH, { providers: {} });
|
|
77
|
-
const accountsState = readJson(ACCOUNTS_STATE_PATH, { providers: {} });
|
|
78
69
|
const claudeUsageSnapshot = readClaudeUsageSnapshot();
|
|
79
70
|
const codexSnapshot = readCodexRateLimitSnapshot();
|
|
80
71
|
const antigravitySnapshot = antigravityAllowed
|
|
@@ -88,11 +79,7 @@ async function main() {
|
|
|
88
79
|
) {
|
|
89
80
|
scheduleClaudeUsageRefresh();
|
|
90
81
|
}
|
|
91
|
-
if (
|
|
92
|
-
showCodex &&
|
|
93
|
-
codexSnapshot.shouldRefresh &&
|
|
94
|
-
(existsSync(join(homedir(), ".codex")) || hasBrokerCodexAccounts())
|
|
95
|
-
) {
|
|
82
|
+
if (showCodex && codexSnapshot.shouldRefresh && existsSync(getCodexHome())) {
|
|
96
83
|
scheduleCodexRateLimitRefresh();
|
|
97
84
|
}
|
|
98
85
|
|
|
@@ -106,9 +93,13 @@ async function main() {
|
|
|
106
93
|
? { ...antigravitySnapshot?.data, auth: getAntigravityAuthKind() }
|
|
107
94
|
: null;
|
|
108
95
|
const currentTier = selectTier();
|
|
96
|
+
// 회색은 로그인 안 된 경우에만 쓴다. 로그인 상태의 조회 공백은 --% 로만 보인다.
|
|
97
|
+
const codexLoggedOut =
|
|
98
|
+
showCodex && !codexBuckets && !existsSync(getCodexAuthPath());
|
|
109
99
|
if (currentTier === "nano") {
|
|
110
100
|
const microLine = getMicroLine(contextView, claudeUsage, codexBuckets, {
|
|
111
101
|
showCodex,
|
|
102
|
+
codexLoggedOut,
|
|
112
103
|
showAntigravity: antigravityAllowed,
|
|
113
104
|
antigravityQuota,
|
|
114
105
|
});
|
|
@@ -128,8 +119,6 @@ async function main() {
|
|
|
128
119
|
"codex",
|
|
129
120
|
"x",
|
|
130
121
|
codexWhite,
|
|
131
|
-
accountsConfig,
|
|
132
|
-
accountsState,
|
|
133
122
|
codexBuckets ? { type: "codex", buckets: codexBuckets } : null,
|
|
134
123
|
getCodexEmail(),
|
|
135
124
|
),
|
|
@@ -142,8 +131,6 @@ async function main() {
|
|
|
142
131
|
"antigravity",
|
|
143
132
|
"a",
|
|
144
133
|
geminiBlue,
|
|
145
|
-
accountsConfig,
|
|
146
|
-
accountsState,
|
|
147
134
|
antigravityQuota,
|
|
148
135
|
getAntigravityAccountLabel(),
|
|
149
136
|
),
|
|
@@ -151,13 +138,7 @@ async function main() {
|
|
|
151
138
|
}
|
|
152
139
|
|
|
153
140
|
const outputLines = renderAlignedRows(rows);
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
outputLines[codexRowIndex] != null &&
|
|
157
|
-
!codexBuckets &&
|
|
158
|
-
!existsSync(CODEX_AUTH_PATH) &&
|
|
159
|
-
!hasBrokerCodexAccounts()
|
|
160
|
-
) {
|
|
141
|
+
if (outputLines[codexRowIndex] != null && codexLoggedOut) {
|
|
161
142
|
outputLines[codexRowIndex] = `${DIM}${outputLines[codexRowIndex]}${RESET}`;
|
|
162
143
|
}
|
|
163
144
|
// 알림 배너와 TUI 스타일이 HUD 내용에 겹치지 않도록 한다.
|
|
@@ -2,16 +2,8 @@
|
|
|
2
2
|
// 세션 이벤트(snake_case) 파이프라인에 합류 가능한 스냅샷을 만든다.
|
|
3
3
|
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import {
|
|
6
|
-
|
|
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
|
|
package/hud/providers/codex.mjs
CHANGED
|
@@ -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(
|
|
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
|
-
//
|
|
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 =
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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 {
|
|
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}
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
+
accountName,
|
|
275
266
|
) {
|
|
276
267
|
const accountLabel = fitText(
|
|
277
|
-
getAccountLabel(provider,
|
|
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
|
@@ -54,7 +54,7 @@ describe("mcp guard engine", () => {
|
|
|
54
54
|
const registry = loadRegistry();
|
|
55
55
|
assert.equal(registry.version, 1);
|
|
56
56
|
assert.equal(registry.servers.context7.url, "https://mcp.context7.com/mcp");
|
|
57
|
-
assert.equal(registry.policies.watched_paths.length,
|
|
57
|
+
assert.equal(registry.policies.watched_paths.length, 7);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
it("drops legacy hub-url servers from a user registry and reads its default transport as http", () => {
|
|
@@ -92,13 +92,13 @@ describe("mcp guard engine", () => {
|
|
|
92
92
|
}
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
it("matches watched paths for
|
|
95
|
+
it("matches watched paths for Antigravity, Claude project MCP, and local .mcp.json", () => {
|
|
96
96
|
const homeDir = createHomeDir();
|
|
97
97
|
withHome(homeDir);
|
|
98
98
|
|
|
99
99
|
assert.equal(
|
|
100
100
|
isWatchedPath(join(homeDir, ".gemini", "settings.json")),
|
|
101
|
-
|
|
101
|
+
false,
|
|
102
102
|
);
|
|
103
103
|
assert.equal(
|
|
104
104
|
isWatchedPath(join(homeDir, ".gemini", "config", "mcp_config.json")),
|
|
@@ -32,9 +32,8 @@ describe("doctor-diagnose: 진단 번들 생성", () => {
|
|
|
32
32
|
assert.equal(typeof result.sysInfo.totalMemMB, "number");
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
it("traceCount
|
|
35
|
+
it("traceCount 숫자", () => {
|
|
36
36
|
assert.equal(typeof result.traceCount, "number");
|
|
37
|
-
assert.equal(typeof result.hookTimingCount, "number");
|
|
38
37
|
});
|
|
39
38
|
|
|
40
39
|
it("codexMcpApproval 진단 결과 포함", () => {
|