@triflux/core 10.43.0 → 10.45.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/hooks/codex-session-hook.mjs +67 -1
- package/hooks/cto-north-star-brief.mjs +3 -16
- package/hooks/error-context.mjs +3 -6
- package/hooks/hook-manager.mjs +1 -1
- package/hooks/hook-registry.json +0 -11
- package/hooks/keyword-rules.json +147 -23
- package/hooks/permission-safe-allow.mjs +36 -2
- package/hooks/session-start-fast.mjs +50 -1
- package/hooks/session-start-lake.mjs +3 -16
- package/hub/bridge.mjs +111 -5
- package/hub/cli-adapter-base.mjs +11 -2
- package/hub/codex-adapter.mjs +3 -0
- package/hub/gemini-adapter.mjs +29 -11
- package/hub/intent.mjs +24 -32
- package/hub/lib/memory-store.mjs +66 -30
- package/hub/lib/tfx-route-args.mjs +41 -4
- package/hub/reflexion.mjs +0 -57
- package/hub/router.mjs +474 -24
- package/hub/team/claude-daemon-control.mjs +2 -0
- package/hub/team/intervention.mjs +1 -1
- package/hub/team/retry-state-machine.mjs +41 -13
- package/hub/team/synapse-http.mjs +149 -4
- package/hud/cli-policy.mjs +69 -0
- package/hud/constants.mjs +20 -17
- package/hud/context-monitor.mjs +31 -21
- package/hud/hud-qos-status.mjs +81 -91
- package/hud/providers/claude.mjs +150 -84
- package/hud/providers/codex.mjs +10 -1
- package/hud/renderers.mjs +21 -59
- package/hud/utils.mjs +1 -15
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +367 -0
- package/scripts/lib/cli-agy.mjs +24 -4
- package/scripts/lib/cli-codex.mjs +22 -160
- package/scripts/lib/codex-profile-config.mjs +48 -4
- package/scripts/lib/gemini-profiles.mjs +113 -29
- package/scripts/lib/keyword-rules.mjs +24 -0
- package/scripts/lib/logger.mjs +60 -43
- package/scripts/lib/machine-profile.mjs +293 -0
- package/scripts/lib/mcp-health.mjs +44 -18
- package/hub/log-retention.mjs +0 -726
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
} from "node:fs";
|
|
24
24
|
import { dirname, join } from "node:path";
|
|
25
25
|
|
|
26
|
+
import { normalizeCodexProfileName } from "../../scripts/lib/codex-profile-config.mjs";
|
|
27
|
+
|
|
26
28
|
export const STATES = Object.freeze({
|
|
27
29
|
PLANNING: "PLANNING",
|
|
28
30
|
EXECUTING: "EXECUTING",
|
|
@@ -39,21 +41,29 @@ export const MODES = Object.freeze({
|
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
const ESCALATION_CHAIN_CONFIG_PATH = ".triflux/config/escalation-chain.json";
|
|
44
|
+
const DEFAULT_CLAUDE_MODEL = "fable";
|
|
42
45
|
|
|
43
|
-
// Escalation chain (2026-
|
|
44
|
-
// 1. codex gpt-
|
|
45
|
-
// 2. claude
|
|
46
|
+
// Escalation chain (2026-09-22 정책):
|
|
47
|
+
// 1. codex gpt-6-astra max: 최난도 단일 작업용 Codex escalation 단계
|
|
48
|
+
// 2. claude fable alias: Claude CLI가 최신 Fable tier로 해석하는 최종 수단
|
|
46
49
|
const DEFAULT_ESCALATION_CHAIN = Object.freeze([
|
|
47
50
|
Object.freeze({
|
|
48
51
|
cli: "codex",
|
|
49
|
-
model: "gpt-
|
|
50
|
-
profile: "
|
|
52
|
+
model: "gpt-6-astra",
|
|
53
|
+
profile: "gpt6_astra_max",
|
|
51
54
|
}),
|
|
52
|
-
Object.freeze({ cli: "claude", model:
|
|
55
|
+
Object.freeze({ cli: "claude", model: DEFAULT_CLAUDE_MODEL }),
|
|
53
56
|
]);
|
|
54
57
|
|
|
55
58
|
const STUCK_THRESHOLD = 3;
|
|
56
59
|
|
|
60
|
+
function normalizeEscalationProfile(profile) {
|
|
61
|
+
const normalized = normalizeCodexProfileName(profile);
|
|
62
|
+
return normalized === "ultra" || normalized === "gpt6_astra_ultra"
|
|
63
|
+
? "gpt6_astra_max"
|
|
64
|
+
: normalized;
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
export function createRetryStateMachine(options = {}) {
|
|
58
68
|
const mode = options.mode || MODES.BOUNDED;
|
|
59
69
|
const cliChain = resolveEscalationChain(options);
|
|
@@ -234,15 +244,25 @@ function resolveEscalationChain(options) {
|
|
|
234
244
|
}
|
|
235
245
|
|
|
236
246
|
const projectRoot = options.projectRoot || process.cwd();
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
247
|
+
const projectOverride = loadEscalationChainOverride(projectRoot);
|
|
248
|
+
if (projectOverride) return projectOverride;
|
|
249
|
+
|
|
250
|
+
return normalizeEscalationChain(
|
|
251
|
+
resolveDefaultEscalationChain(options.env || process.env),
|
|
252
|
+
"DEFAULT_ESCALATION_CHAIN",
|
|
243
253
|
);
|
|
244
254
|
}
|
|
245
255
|
|
|
256
|
+
function resolveDefaultEscalationChain(env) {
|
|
257
|
+
const configuredModel = env?.TFX_ESCALATION_CLAUDE_MODEL?.trim();
|
|
258
|
+
if (!configuredModel) return DEFAULT_ESCALATION_CHAIN;
|
|
259
|
+
|
|
260
|
+
return [
|
|
261
|
+
DEFAULT_ESCALATION_CHAIN[0],
|
|
262
|
+
{ cli: "claude", model: configuredModel },
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
|
|
246
266
|
export function loadEscalationChainOverride(projectRoot = process.cwd()) {
|
|
247
267
|
const configPath = join(projectRoot, ESCALATION_CHAIN_CONFIG_PATH);
|
|
248
268
|
if (!existsSync(configPath)) return null;
|
|
@@ -295,7 +315,9 @@ function normalizeEscalationChainEntry(entry, index, source) {
|
|
|
295
315
|
model: entry.model,
|
|
296
316
|
};
|
|
297
317
|
if (typeof entry.profile === "string" && entry.profile.trim() !== "") {
|
|
298
|
-
|
|
318
|
+
const profile = entry.profile.trim();
|
|
319
|
+
normalized.profile =
|
|
320
|
+
entry.cli === "codex" ? normalizeEscalationProfile(profile) : profile;
|
|
299
321
|
}
|
|
300
322
|
return normalized;
|
|
301
323
|
}
|
|
@@ -328,6 +350,12 @@ export function loadSnapshot(snapshotFile) {
|
|
|
328
350
|
`unsupported snapshot version: ${parsed.version} (expected 1)`,
|
|
329
351
|
);
|
|
330
352
|
}
|
|
353
|
+
if (Array.isArray(parsed.cliChain) && parsed.cliChain.length > 0) {
|
|
354
|
+
return {
|
|
355
|
+
...parsed,
|
|
356
|
+
cliChain: normalizeEscalationChain(parsed.cliChain, "snapshot.cliChain"),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
331
359
|
return parsed;
|
|
332
360
|
}
|
|
333
361
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { homedir, hostname } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { normalizeRoleKey } from "../role-contract.mjs";
|
|
4
7
|
|
|
5
8
|
const HUB_DEFAULT_PORT = 27888;
|
|
6
9
|
// Same token file as hub/bridge.mjs (HUB_TOKEN_FILE). Kept inline rather than
|
|
@@ -21,6 +24,144 @@ function normalizeToken(raw) {
|
|
|
21
24
|
return token || null;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
function opaqueId(prefix, value) {
|
|
28
|
+
return `${prefix}_${createHash("sha256")
|
|
29
|
+
.update(String(value || ""))
|
|
30
|
+
.digest("base64url")
|
|
31
|
+
.slice(0, 22)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizedProjectId(value) {
|
|
35
|
+
try {
|
|
36
|
+
return normalizeRoleKey({ project_id: value, role_kind: "cto" }).project_id;
|
|
37
|
+
} catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveProjectId(cwd, meta, opts = {}) {
|
|
43
|
+
const explicit = meta?.project_id || process.env.TFX_PROJECT_ID;
|
|
44
|
+
if (explicit) return normalizedProjectId(explicit);
|
|
45
|
+
if (typeof opts.resolveProjectId === "function") {
|
|
46
|
+
return normalizedProjectId(opts.resolveProjectId(cwd));
|
|
47
|
+
}
|
|
48
|
+
let current = cwd;
|
|
49
|
+
while (current) {
|
|
50
|
+
const manifest = join(current, ".triflux", "project.json");
|
|
51
|
+
if (existsSync(manifest)) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
54
|
+
if (parsed?.schema_version !== "tfx.project.v1") return "";
|
|
55
|
+
return normalizedProjectId(parsed.project_id);
|
|
56
|
+
} catch {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const parent = dirname(current);
|
|
61
|
+
if (parent === current) break;
|
|
62
|
+
current = parent;
|
|
63
|
+
}
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function inferSessionCli(meta, opts = {}) {
|
|
68
|
+
const explicit = String(meta?.cli || meta?.actor_cli || "")
|
|
69
|
+
.trim()
|
|
70
|
+
.toLowerCase();
|
|
71
|
+
if (explicit === "codex" || explicit === "claude") return explicit;
|
|
72
|
+
const entrypoint = String(opts.entrypoint ?? process.argv[1] ?? "");
|
|
73
|
+
if (entrypoint.includes("codex-session-hook")) return "codex";
|
|
74
|
+
if (entrypoint.includes("agy-session-hook")) return "other";
|
|
75
|
+
return "claude";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveTmuxSession(meta, opts = {}) {
|
|
79
|
+
const explicit =
|
|
80
|
+
meta?.tmux_session || meta?.session_name || process.env.TFX_TMUX_SESSION;
|
|
81
|
+
if (explicit) return String(explicit).trim();
|
|
82
|
+
if (!process.env.TMUX) return "";
|
|
83
|
+
try {
|
|
84
|
+
const run =
|
|
85
|
+
opts.tmuxSessionName ||
|
|
86
|
+
(() =>
|
|
87
|
+
execFileSync("tmux", ["display-message", "-p", "#{session_name}"], {
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
timeout: 200,
|
|
90
|
+
windowsHide: true,
|
|
91
|
+
}));
|
|
92
|
+
return String(run() || "").trim();
|
|
93
|
+
} catch {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildSynapseRegistrationMeta(meta, opts = {}) {
|
|
99
|
+
if (!meta || meta.sessionKind !== "interactive") return meta;
|
|
100
|
+
const sessionId = String(meta.sessionId || "").trim();
|
|
101
|
+
if (!sessionId) return meta;
|
|
102
|
+
const cwd = typeof meta.cwd === "string" ? meta.cwd : process.cwd();
|
|
103
|
+
const cli = inferSessionCli(meta, opts);
|
|
104
|
+
const hostId = String(
|
|
105
|
+
meta.host_id ||
|
|
106
|
+
process.env.TFX_HOST_ID ||
|
|
107
|
+
opaqueId("hst", opts.hostname?.() || hostname()),
|
|
108
|
+
).trim();
|
|
109
|
+
const expiresAtMs = Number(opts.nowMs?.() ?? Date.now()) + 299000;
|
|
110
|
+
const transportLocators = [];
|
|
111
|
+
if (cli === "claude") {
|
|
112
|
+
transportLocators.push({
|
|
113
|
+
schema_version: "tfx.transport-locator.v1",
|
|
114
|
+
transport_id: opaqueId("trn", `claude-uds:${hostId}:${sessionId}`),
|
|
115
|
+
kind: "claude-uds",
|
|
116
|
+
host_id: hostId,
|
|
117
|
+
capabilities: ["probe", "wake", "activate", "interrupt"],
|
|
118
|
+
priority: 100,
|
|
119
|
+
expires_at_ms: expiresAtMs,
|
|
120
|
+
locator: {
|
|
121
|
+
session_id: sessionId,
|
|
122
|
+
bridge_id: String(
|
|
123
|
+
meta.bridge_id || process.env.TFX_BRIDGE_ID || "local-hub",
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const tmuxSession = resolveTmuxSession(meta, opts);
|
|
129
|
+
if (tmuxSession) {
|
|
130
|
+
const muxServerSource =
|
|
131
|
+
meta.mux_server_id ||
|
|
132
|
+
process.env.TFX_MUX_SERVER_ID ||
|
|
133
|
+
String(process.env.TMUX || "default").split(",", 1)[0];
|
|
134
|
+
transportLocators.push({
|
|
135
|
+
schema_version: "tfx.transport-locator.v1",
|
|
136
|
+
transport_id: opaqueId("trn", `tmux:${hostId}:${tmuxSession}`),
|
|
137
|
+
kind: "tmux",
|
|
138
|
+
host_id: hostId,
|
|
139
|
+
capabilities: ["probe", "wake", "activate", "interrupt"],
|
|
140
|
+
priority: cli === "codex" ? 100 : 50,
|
|
141
|
+
expires_at_ms: expiresAtMs,
|
|
142
|
+
locator: {
|
|
143
|
+
session_name: tmuxSession,
|
|
144
|
+
mux_server_id: opaqueId("mux", muxServerSource),
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const directAgentId = `session:${sessionId}`;
|
|
149
|
+
return {
|
|
150
|
+
...meta,
|
|
151
|
+
agent_id:
|
|
152
|
+
meta.agent_id ||
|
|
153
|
+
(directAgentId.length <= 64 && /^[a-zA-Z0-9._:-]+$/u.test(directAgentId)
|
|
154
|
+
? directAgentId
|
|
155
|
+
: opaqueId("session", sessionId)),
|
|
156
|
+
cli,
|
|
157
|
+
project_id: resolveProjectId(cwd, meta, opts) || undefined,
|
|
158
|
+
session_id: meta.session_id || sessionId,
|
|
159
|
+
host_id: hostId,
|
|
160
|
+
transport_locators_json:
|
|
161
|
+
meta.transport_locators_json ?? JSON.stringify(transportLocators),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
24
165
|
// Mirrors hub/bridge.mjs:readHubToken — env override first, then the token file.
|
|
25
166
|
// Missing token → null (backward-compatible with token-less hubs).
|
|
26
167
|
function readHubToken() {
|
|
@@ -139,7 +280,11 @@ export function drainPendingSynapse(timeoutMs = 1000) {
|
|
|
139
280
|
}
|
|
140
281
|
|
|
141
282
|
export function registerSynapseSession(meta, opts = {}) {
|
|
142
|
-
return fireAndForgetSynapse(
|
|
283
|
+
return fireAndForgetSynapse(
|
|
284
|
+
"/synapse/register",
|
|
285
|
+
buildSynapseRegistrationMeta(meta, opts),
|
|
286
|
+
opts,
|
|
287
|
+
);
|
|
143
288
|
}
|
|
144
289
|
|
|
145
290
|
export function heartbeatSynapseSession(
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// HUD CLI 표시 정책 판정 (순수 함수)
|
|
2
|
+
//
|
|
3
|
+
// machine profile 의 TFX_DISABLE_CODEX / TFX_DISABLE_ANTIGRAVITY 를 읽어서 HUD 가
|
|
4
|
+
// 어떤 프로바이더 행을 그릴지 정한다. 판독 자체는 scripts/lib/machine-profile.mjs
|
|
5
|
+
// 가 단일 정본이고, 이 모듈은 그 결과를 HUD 표시 결정으로 접기만 한다.
|
|
6
|
+
//
|
|
7
|
+
// 상대 경로 import 를 고정하는 이유: setup 이 hud/** 를 ~/.claude/hud/ 로,
|
|
8
|
+
// scripts/lib/* 를 ~/.claude/scripts/lib/ 로 각각 동기화한다. 그래서 설치본에서도
|
|
9
|
+
// ../scripts/lib/ 가 repo 와 같은 파일을 가리킨다. packages/core 와
|
|
10
|
+
// packages/triflux 미러도 같은 상대 레이아웃이다.
|
|
11
|
+
//
|
|
12
|
+
// fail-open 원칙: HUD 는 statusline 이라 어떤 예외에서도 죽으면 안 된다. 정책
|
|
13
|
+
// 판독이 실패하면 전부 표시하는 쪽으로 돌아간다.
|
|
14
|
+
|
|
15
|
+
import { resolveCliPolicy } from "../scripts/lib/machine-profile.mjs";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* HUD 가 codex / antigravity 행을 그릴 수 있는지 판정한다.
|
|
19
|
+
*
|
|
20
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
21
|
+
* @param {{
|
|
22
|
+
* policy?: ReturnType<typeof resolveCliPolicy>|null,
|
|
23
|
+
* resolve?: (env: NodeJS.ProcessEnv, opts: object) => ReturnType<typeof resolveCliPolicy>,
|
|
24
|
+
* profile?: object,
|
|
25
|
+
* platform?: string,
|
|
26
|
+
* home?: string,
|
|
27
|
+
* }} [opts] policy 는 이미 계산한 정책 주입, resolve 는 테스트용 판독 시임이다.
|
|
28
|
+
* @returns {{ showCodex: boolean, antigravityAllowed: boolean, policy: object|null }}
|
|
29
|
+
*/
|
|
30
|
+
export function resolveHudCliVisibility(env = process.env, opts = {}) {
|
|
31
|
+
try {
|
|
32
|
+
const resolver =
|
|
33
|
+
typeof opts.resolve === "function" ? opts.resolve : resolveCliPolicy;
|
|
34
|
+
const policy = opts.policy ?? resolver(env, opts);
|
|
35
|
+
return {
|
|
36
|
+
showCodex: policy?.codexDisabled !== true,
|
|
37
|
+
antigravityAllowed: policy?.antigravityDisabled !== true,
|
|
38
|
+
policy: policy ?? null,
|
|
39
|
+
};
|
|
40
|
+
} catch {
|
|
41
|
+
return { showCodex: true, antigravityAllowed: true, policy: null };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* antigravity / gemini 슬롯 행을 그릴지 판정한다.
|
|
47
|
+
*
|
|
48
|
+
* antigravity 가 허용이면 기존 동작 그대로 항상 그린다. 정책으로 꺼졌을 때만
|
|
49
|
+
* gemini 폴백 정보가 하나라도 있는지 보고, 전부 비어 있으면 행을 생략한다.
|
|
50
|
+
*
|
|
51
|
+
* @param {{
|
|
52
|
+
* antigravityAllowed?: boolean,
|
|
53
|
+
* antigravityReady?: boolean,
|
|
54
|
+
* geminiEmail?: unknown,
|
|
55
|
+
* geminiBucket?: unknown,
|
|
56
|
+
* geminiSession?: unknown,
|
|
57
|
+
* }} [args]
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
export function shouldRenderGeminiFallbackRow({
|
|
61
|
+
antigravityAllowed = true,
|
|
62
|
+
antigravityReady = false,
|
|
63
|
+
geminiEmail = null,
|
|
64
|
+
geminiBucket = null,
|
|
65
|
+
geminiSession = null,
|
|
66
|
+
} = {}) {
|
|
67
|
+
if (antigravityAllowed || antigravityReady) return true;
|
|
68
|
+
return Boolean(geminiEmail || geminiBucket || geminiSession);
|
|
69
|
+
}
|
package/hud/constants.mjs
CHANGED
|
@@ -71,7 +71,7 @@ export const CONTEXT_MONITOR_LOG_DIR = join(
|
|
|
71
71
|
"logs",
|
|
72
72
|
);
|
|
73
73
|
|
|
74
|
-
// 원격 프로브 캐시 (tfx-remote
|
|
74
|
+
// 원격 프로브 캐시 (tfx-remote)
|
|
75
75
|
export const REMOTE_ENV_CACHE_DIR = join(
|
|
76
76
|
homedir(),
|
|
77
77
|
".claude",
|
|
@@ -89,12 +89,19 @@ export const CLAUDE_CREDENTIALS_PATH = join(
|
|
|
89
89
|
);
|
|
90
90
|
export const CLAUDE_CREDENTIALS_FILENAME = ".credentials.json";
|
|
91
91
|
export const CLAUDE_CODE_USER_AGENT_FALLBACK = "claude-code/2.x";
|
|
92
|
+
// Claude Code 2.1.x 평문 폴백 파일 위치: CLAUDE_SECURESTORAGE_CONFIG_DIR 가 정의돼 있으면 그 디렉터리
|
|
93
|
+
// (빈 문자열은 기본 ~/.claude), 아니면 CLAUDE_CONFIG_DIR. 기본 경로는 항상 마지막 후보로 둔다.
|
|
92
94
|
export function getClaudeCredentialPaths(env = process.env) {
|
|
93
95
|
const paths = [];
|
|
94
|
-
|
|
96
|
+
const secureDir = env.CLAUDE_SECURESTORAGE_CONFIG_DIR;
|
|
97
|
+
if (secureDir !== undefined) {
|
|
98
|
+
if (secureDir) {
|
|
99
|
+
paths.push(join(secureDir.normalize("NFC"), CLAUDE_CREDENTIALS_FILENAME));
|
|
100
|
+
}
|
|
101
|
+
} else if (env.CLAUDE_CONFIG_DIR) {
|
|
95
102
|
paths.push(join(env.CLAUDE_CONFIG_DIR, CLAUDE_CREDENTIALS_FILENAME));
|
|
96
103
|
}
|
|
97
|
-
paths.push(CLAUDE_CREDENTIALS_PATH);
|
|
104
|
+
if (paths[0] !== CLAUDE_CREDENTIALS_PATH) paths.push(CLAUDE_CREDENTIALS_PATH);
|
|
98
105
|
return paths;
|
|
99
106
|
}
|
|
100
107
|
export function getClaudeCodeUserAgent(env = process.env) {
|
|
@@ -198,7 +205,16 @@ export const ANTIGRAVITY_SETTINGS_PATH = join(
|
|
|
198
205
|
// /usage 실측 (2026-05-22): Gemini 4개 모델이 동일 reset window 공유 (family-pool).
|
|
199
206
|
// Claude/GPT-OSS family는 Gemini와 별도 quota 윈도우.
|
|
200
207
|
export const ANTIGRAVITY_MODEL_ABBREV = {
|
|
201
|
-
// Gemini family — 통합 quota
|
|
208
|
+
// Gemini family — 통합 quota. 3.6+ Flash 는 세대 숫자 + 등급으로 구분한다.
|
|
209
|
+
"Gemini 3.8 Flash (High)": "8h",
|
|
210
|
+
"Gemini 3.8 Flash (Medium)": "8m",
|
|
211
|
+
"Gemini 3.8 Flash (Low)": "8l",
|
|
212
|
+
"Gemini 3.7 Flash (High)": "7h",
|
|
213
|
+
"Gemini 3.7 Flash (Medium)": "7m",
|
|
214
|
+
"Gemini 3.7 Flash (Low)": "7l",
|
|
215
|
+
"Gemini 3.6 Flash (High)": "6h",
|
|
216
|
+
"Gemini 3.6 Flash (Medium)": "6m",
|
|
217
|
+
"Gemini 3.6 Flash (Low)": "6l",
|
|
202
218
|
"Gemini 3.5 Flash (High)": "Fh",
|
|
203
219
|
"Gemini 3.5 Flash (Medium)": "Fm",
|
|
204
220
|
"Gemini 3.5 Flash (Low)": "Fl",
|
|
@@ -242,12 +258,6 @@ export const GEMINI_RPM_TRACKER_PATH = join(
|
|
|
242
258
|
"cache",
|
|
243
259
|
"gemini-rpm-tracker.json",
|
|
244
260
|
);
|
|
245
|
-
export const SV_ACCUMULATOR_PATH = join(
|
|
246
|
-
homedir(),
|
|
247
|
-
".claude",
|
|
248
|
-
"cache",
|
|
249
|
-
"sv-accumulator.json",
|
|
250
|
-
);
|
|
251
261
|
// 이전 .omc/ 경로 fallback (기존 환경 호환)
|
|
252
262
|
export const LEGACY_GEMINI_QUOTA_CACHE = join(
|
|
253
263
|
homedir(),
|
|
@@ -273,12 +283,6 @@ export const LEGACY_GEMINI_RPM_TRACKER = join(
|
|
|
273
283
|
"state",
|
|
274
284
|
"gemini_rpm_tracker.json",
|
|
275
285
|
);
|
|
276
|
-
export const LEGACY_SV_ACCUMULATOR = join(
|
|
277
|
-
homedir(),
|
|
278
|
-
".omc",
|
|
279
|
-
"state",
|
|
280
|
-
"sv-accumulator.json",
|
|
281
|
-
);
|
|
282
286
|
|
|
283
287
|
export const GEMINI_RPM_WINDOW_MS = 60 * 1000; // 60초 슬라이딩 윈도우
|
|
284
288
|
export const GEMINI_QUOTA_STALE_MS = 5 * 60 * 1000; // 5분
|
|
@@ -289,7 +293,6 @@ export const ACCOUNT_LABEL_WIDTH = 10;
|
|
|
289
293
|
export const PROVIDER_PREFIX_WIDTH = 2;
|
|
290
294
|
export const PERCENT_CELL_WIDTH = 3;
|
|
291
295
|
export const TIME_CELL_INNER_WIDTH = 6;
|
|
292
|
-
export const SV_CELL_WIDTH = 5;
|
|
293
296
|
|
|
294
297
|
export const CLAUDE_REFRESH_FLAG = "--refresh-claude-usage";
|
|
295
298
|
export const CODEX_REFRESH_FLAG = "--refresh-codex-rate-limits";
|
package/hud/context-monitor.mjs
CHANGED
|
@@ -15,13 +15,12 @@ const MAX_CAPTURE_BYTES = 256 * 1024;
|
|
|
15
15
|
const MAX_TOP_KEYS = 20;
|
|
16
16
|
|
|
17
17
|
// stdin 이 context_window_size 를 제공하지 않을 때 모델 ID 로 한도를 추정한다.
|
|
18
|
-
// Anthropic 공식 문서(2026-
|
|
19
|
-
// Sonnet 4.5 / Haiku 4.5 = 200K
|
|
18
|
+
// Anthropic 공식 문서(2026-09-22 기준): Opus 4.6 이상, Sonnet 4.6/5,
|
|
19
|
+
// Fable 5/5.1 = 1M. Sonnet 4.5 / Haiku 4.5 = 200K이며 [1m]도 유지한다.
|
|
20
20
|
const MODEL_HINT_1M_PREFIXES = [
|
|
21
|
-
"claude-opus-4-8",
|
|
22
|
-
"claude-opus-4-7",
|
|
23
|
-
"claude-opus-4-6",
|
|
24
21
|
"claude-sonnet-4-6",
|
|
22
|
+
"claude-sonnet-5",
|
|
23
|
+
"claude-fable-5",
|
|
25
24
|
];
|
|
26
25
|
|
|
27
26
|
function normalizeModelId(modelId) {
|
|
@@ -29,24 +28,34 @@ function normalizeModelId(modelId) {
|
|
|
29
28
|
return String(modelId).toLowerCase();
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
function
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return DEFAULT_CONTEXT_LIMIT;
|
|
31
|
+
function isMillionContextOpusModel(modelId) {
|
|
32
|
+
const match = /^claude-opus-(\d+)(?:-(\d+))?(?:$|[-[\s])/u.exec(modelId);
|
|
33
|
+
if (!match) return false;
|
|
34
|
+
|
|
35
|
+
const major = Number(match[1]);
|
|
36
|
+
const minor = Number(match[2] || 0);
|
|
37
|
+
return major > 4 || (major === 4 && minor >= 6);
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
function isMillionContextModel(modelId) {
|
|
43
41
|
const id = normalizeModelId(modelId);
|
|
44
42
|
if (!id) return false;
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
if (id.includes("[1m]")) return true;
|
|
44
|
+
if (isMillionContextOpusModel(id)) return true;
|
|
45
|
+
return MODEL_HINT_1M_PREFIXES.some((prefix) => id.startsWith(prefix));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveModelLimit(modelId) {
|
|
49
|
+
return isMillionContextModel(modelId)
|
|
50
|
+
? MILLION_CONTEXT_LIMIT
|
|
51
|
+
: DEFAULT_CONTEXT_LIMIT;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function shouldSuppressInfoOnlyContextStatus(modelId, contextLimit = 0) {
|
|
55
|
+
const explicitLimit = Number(contextLimit);
|
|
56
|
+
return explicitLimit > 0
|
|
57
|
+
? explicitLimit >= MILLION_CONTEXT_LIMIT
|
|
58
|
+
: isMillionContextModel(modelId);
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
const WARNING_LEVELS = Object.freeze({
|
|
@@ -282,6 +291,7 @@ export function deriveContextLimit(stdin) {
|
|
|
282
291
|
}
|
|
283
292
|
|
|
284
293
|
export function buildContextUsageView(stdin, snapshot = null) {
|
|
294
|
+
const modelId = stdin?.model?.id ?? stdin?.model;
|
|
285
295
|
const stdinUsage = getStdinContextUsage(stdin);
|
|
286
296
|
if (!stdinUsage) {
|
|
287
297
|
return {
|
|
@@ -296,14 +306,14 @@ export function buildContextUsageView(stdin, snapshot = null) {
|
|
|
296
306
|
};
|
|
297
307
|
}
|
|
298
308
|
|
|
299
|
-
const modelId = stdin?.model?.id ?? stdin?.model;
|
|
300
309
|
const limitTokens = Math.max(1, stdinUsage.limitTokens);
|
|
301
310
|
const usedTokens = stdinUsage.usedTokens;
|
|
302
311
|
const percent = clampPercent((usedTokens / limitTokens) * 100);
|
|
303
312
|
|
|
304
313
|
const warning = classifyContextThreshold(percent);
|
|
305
314
|
const showInfoOnlyStatus = !(
|
|
306
|
-
warning.level === "info" &&
|
|
315
|
+
warning.level === "info" &&
|
|
316
|
+
shouldSuppressInfoOnlyContextStatus(modelId, stdinUsage.limitTokens)
|
|
307
317
|
);
|
|
308
318
|
return {
|
|
309
319
|
usedTokens,
|