@triflux/core 10.1.0 → 10.26.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/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +191 -31
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +262 -34
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +1051 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +22 -21
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/schema/delegator-tools.schema.json +1 -1
- package/hub/delegator/service.mjs +93 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +82 -108
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +78 -30
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/claude-daemon-control.mjs +1130 -0
- package/hub/team/retry-state-machine.mjs +336 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +23 -17
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +190 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +157 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +210 -52
- package/hud/renderers.mjs +440 -127
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +37 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +49 -20
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +117 -12
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
// hub/workers/worker-utils.mjs — 워커 공통 유틸리티
|
|
2
|
-
// claude-worker,
|
|
2
|
+
// claude-worker, Antigravity route compatibility worker, pipe 등에서 공유하는 순수 유틸 함수 모음.
|
|
3
3
|
|
|
4
4
|
export const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
5
5
|
export const DEFAULT_KILL_GRACE_MS = 1000;
|
|
6
6
|
|
|
7
7
|
export function toStringList(value) {
|
|
8
8
|
if (!Array.isArray(value)) return [];
|
|
9
|
-
return value
|
|
10
|
-
.map((item) => String(item ?? '').trim())
|
|
11
|
-
.filter(Boolean);
|
|
9
|
+
return value.map((item) => String(item ?? "").trim()).filter(Boolean);
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
export function safeJsonParse(line) {
|
|
@@ -27,8 +25,7 @@ export function createWorkerError(message, details = {}) {
|
|
|
27
25
|
|
|
28
26
|
function sleep(delayMs) {
|
|
29
27
|
return new Promise((resolve) => {
|
|
30
|
-
|
|
31
|
-
timer.unref?.();
|
|
28
|
+
setTimeout(resolve, Math.max(0, delayMs));
|
|
32
29
|
});
|
|
33
30
|
}
|
|
34
31
|
|
|
@@ -51,8 +48,9 @@ export async function withRetry(fn, opts = {}) {
|
|
|
51
48
|
throw error;
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
const delay =
|
|
55
|
-
* (
|
|
51
|
+
const delay =
|
|
52
|
+
Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs) *
|
|
53
|
+
(0.5 + Math.random() * 0.5);
|
|
56
54
|
await sleep(delay);
|
|
57
55
|
}
|
|
58
56
|
}
|
|
@@ -63,7 +61,7 @@ export async function withRetry(fn, opts = {}) {
|
|
|
63
61
|
export function appendTextFragments(value, parts) {
|
|
64
62
|
if (value == null) return;
|
|
65
63
|
|
|
66
|
-
if (typeof value ===
|
|
64
|
+
if (typeof value === "string") {
|
|
67
65
|
const trimmed = value.trim();
|
|
68
66
|
if (trimmed) parts.push(trimmed);
|
|
69
67
|
return;
|
|
@@ -74,11 +72,13 @@ export function appendTextFragments(value, parts) {
|
|
|
74
72
|
return;
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
if (typeof value !==
|
|
75
|
+
if (typeof value !== "object") return;
|
|
78
76
|
|
|
79
|
-
if (typeof value.text ===
|
|
80
|
-
if (typeof value.response ===
|
|
81
|
-
|
|
77
|
+
if (typeof value.text === "string") appendTextFragments(value.text, parts);
|
|
78
|
+
if (typeof value.response === "string")
|
|
79
|
+
appendTextFragments(value.response, parts);
|
|
80
|
+
if (typeof value.result === "string")
|
|
81
|
+
appendTextFragments(value.result, parts);
|
|
82
82
|
if (value.content != null) appendTextFragments(value.content, parts);
|
|
83
83
|
if (value.message != null) appendTextFragments(value.message, parts);
|
|
84
84
|
}
|
|
@@ -86,18 +86,24 @@ export function appendTextFragments(value, parts) {
|
|
|
86
86
|
export function extractText(value) {
|
|
87
87
|
const parts = [];
|
|
88
88
|
appendTextFragments(value, parts);
|
|
89
|
-
return parts.join(
|
|
89
|
+
return parts.join("\n").trim();
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export function terminateChild(child, killGraceMs) {
|
|
93
93
|
if (!child || child.exitCode !== null || child.killed) return;
|
|
94
94
|
|
|
95
|
-
try {
|
|
96
|
-
|
|
95
|
+
try {
|
|
96
|
+
child.stdin.end();
|
|
97
|
+
} catch {}
|
|
98
|
+
try {
|
|
99
|
+
child.kill();
|
|
100
|
+
} catch {}
|
|
97
101
|
|
|
98
102
|
const timer = setTimeout(() => {
|
|
99
103
|
if (child.exitCode === null) {
|
|
100
|
-
try {
|
|
104
|
+
try {
|
|
105
|
+
child.kill("SIGKILL");
|
|
106
|
+
} catch {}
|
|
101
107
|
}
|
|
102
108
|
}, killGraceMs);
|
|
103
109
|
timer.unref?.();
|
package/hud/colors.mjs
CHANGED
|
@@ -12,15 +12,33 @@ export const CLAUDE_ORANGE = "\x1b[38;2;232;112;64m"; // #E87040 (Claude 공식
|
|
|
12
12
|
export const CODEX_WHITE = "\x1b[97m"; // bright white (SGR 37은 Windows Terminal에서 연회색 매핑)
|
|
13
13
|
export const GEMINI_BLUE = "\x1b[38;5;39m";
|
|
14
14
|
|
|
15
|
-
export function green(t) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export function
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
export function green(t) {
|
|
16
|
+
return `${GREEN}${t}${RESET}`;
|
|
17
|
+
}
|
|
18
|
+
export function yellow(t) {
|
|
19
|
+
return `${YELLOW}${t}${RESET}`;
|
|
20
|
+
}
|
|
21
|
+
export function red(t) {
|
|
22
|
+
return `${RED}${t}${RESET}`;
|
|
23
|
+
}
|
|
24
|
+
export function cyan(t) {
|
|
25
|
+
return `${CYAN}${t}${RESET}`;
|
|
26
|
+
}
|
|
27
|
+
export function dim(t) {
|
|
28
|
+
return `${DIM}${t}${RESET}`;
|
|
29
|
+
}
|
|
30
|
+
export function bold(t) {
|
|
31
|
+
return `${BOLD}${t}${RESET}`;
|
|
32
|
+
}
|
|
33
|
+
export function claudeOrange(t) {
|
|
34
|
+
return `${CLAUDE_ORANGE}${t}${RESET}`;
|
|
35
|
+
}
|
|
36
|
+
export function codexWhite(t) {
|
|
37
|
+
return `${CODEX_WHITE}${t}${RESET}`;
|
|
38
|
+
}
|
|
39
|
+
export function geminiBlue(t) {
|
|
40
|
+
return `${GEMINI_BLUE}${t}${RESET}`;
|
|
41
|
+
}
|
|
24
42
|
|
|
25
43
|
export function colorByPercent(value, text) {
|
|
26
44
|
if (value >= 85) return red(text);
|
package/hud/constants.mjs
CHANGED
|
@@ -6,26 +6,88 @@ import { join } from "node:path";
|
|
|
6
6
|
|
|
7
7
|
export const VERSION = "2.0";
|
|
8
8
|
|
|
9
|
-
export const QOS_PATH = join(
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
export const QOS_PATH = join(
|
|
10
|
+
homedir(),
|
|
11
|
+
".claude",
|
|
12
|
+
"cache",
|
|
13
|
+
"tfx-hub",
|
|
14
|
+
"cli_qos_profile.json",
|
|
15
|
+
);
|
|
16
|
+
export const ACCOUNTS_CONFIG_PATH = join(
|
|
17
|
+
homedir(),
|
|
18
|
+
".claude",
|
|
19
|
+
"cache",
|
|
20
|
+
"tfx-hub",
|
|
21
|
+
"accounts.json",
|
|
22
|
+
);
|
|
23
|
+
export const ACCOUNTS_STATE_PATH = join(
|
|
24
|
+
homedir(),
|
|
25
|
+
".claude",
|
|
26
|
+
"cache",
|
|
27
|
+
"tfx-hub",
|
|
28
|
+
"cli_accounts_state.json",
|
|
29
|
+
);
|
|
12
30
|
|
|
13
31
|
// tfx-multi 상태 (v2.2 HUD 통합)
|
|
14
|
-
export const TEAM_STATE_PATH = join(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
32
|
+
export const TEAM_STATE_PATH = join(
|
|
33
|
+
homedir(),
|
|
34
|
+
".claude",
|
|
35
|
+
"cache",
|
|
36
|
+
"tfx-hub",
|
|
37
|
+
"team-state.json",
|
|
38
|
+
);
|
|
39
|
+
export const CONTEXT_MONITOR_CACHE_PATH = join(
|
|
40
|
+
homedir(),
|
|
41
|
+
".claude",
|
|
42
|
+
"cache",
|
|
43
|
+
"tfx-hub",
|
|
44
|
+
"context-monitor.json",
|
|
45
|
+
);
|
|
46
|
+
export const CONTEXT_MONITOR_LEGACY_PATH = join(
|
|
47
|
+
homedir(),
|
|
48
|
+
".omc",
|
|
49
|
+
"state",
|
|
50
|
+
"context-monitor.json",
|
|
51
|
+
);
|
|
52
|
+
export const CONTEXT_MONITOR_LOG_DIR = join(
|
|
53
|
+
homedir(),
|
|
54
|
+
".claude",
|
|
55
|
+
"cache",
|
|
56
|
+
"tfx-hub",
|
|
57
|
+
"logs",
|
|
58
|
+
);
|
|
18
59
|
|
|
19
60
|
// 원격 프로브 캐시 (tfx-remote-spawn)
|
|
20
|
-
export const REMOTE_ENV_CACHE_DIR = join(
|
|
61
|
+
export const REMOTE_ENV_CACHE_DIR = join(
|
|
62
|
+
homedir(),
|
|
63
|
+
".claude",
|
|
64
|
+
"cache",
|
|
65
|
+
"tfx-hub",
|
|
66
|
+
"remote-env",
|
|
67
|
+
);
|
|
21
68
|
export const REMOTE_ENV_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24시간
|
|
22
69
|
|
|
23
70
|
// Claude OAuth Usage API (api.anthropic.com/api/oauth/usage)
|
|
24
|
-
export const CLAUDE_CREDENTIALS_PATH = join(
|
|
25
|
-
|
|
26
|
-
|
|
71
|
+
export const CLAUDE_CREDENTIALS_PATH = join(
|
|
72
|
+
homedir(),
|
|
73
|
+
".claude",
|
|
74
|
+
".credentials.json",
|
|
75
|
+
);
|
|
76
|
+
export const CLAUDE_USAGE_CACHE_PATH = join(
|
|
77
|
+
homedir(),
|
|
78
|
+
".claude",
|
|
79
|
+
"cache",
|
|
80
|
+
"claude-usage-cache.json",
|
|
81
|
+
);
|
|
82
|
+
export const OMC_PLUGIN_USAGE_CACHE_PATH = join(
|
|
83
|
+
homedir(),
|
|
84
|
+
".claude",
|
|
85
|
+
"cache",
|
|
86
|
+
"tfx-hub",
|
|
87
|
+
"plugin-usage-cache.json",
|
|
88
|
+
);
|
|
27
89
|
export const CLAUDE_USAGE_STALE_MS_SOLO = 5 * 60 * 1000; // OMC 없을 때: 5분 캐시
|
|
28
|
-
export const CLAUDE_USAGE_STALE_MS_WITH_OMC = 15 * 60 * 1000; // OMC 있을 때: 15분
|
|
90
|
+
export const CLAUDE_USAGE_STALE_MS_WITH_OMC = 15 * 60 * 1000; // OMC 있을 때: 15분
|
|
29
91
|
export const CLAUDE_USAGE_429_BACKOFF_MS = 10 * 60 * 1000; // 429 에러 시 10분 backoff
|
|
30
92
|
export const GEMINI_429_BASE_DELAY_MS = 2000;
|
|
31
93
|
export const GEMINI_429_MAX_RETRIES = 3;
|
|
@@ -38,27 +100,121 @@ export const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
|
38
100
|
export const DEFAULT_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
39
101
|
|
|
40
102
|
export const CODEX_AUTH_PATH = join(homedir(), ".codex", "auth.json");
|
|
41
|
-
export const CODEX_QUOTA_CACHE_PATH = join(
|
|
42
|
-
|
|
103
|
+
export const CODEX_QUOTA_CACHE_PATH = join(
|
|
104
|
+
homedir(),
|
|
105
|
+
".claude",
|
|
106
|
+
"cache",
|
|
107
|
+
"codex-rate-limits-cache.json",
|
|
108
|
+
);
|
|
109
|
+
export const CODEX_QUOTA_STALE_MS = 30 * 1000; // 30초
|
|
43
110
|
export const CODEX_MIN_BUCKETS = 2;
|
|
44
111
|
|
|
112
|
+
// Spawn lock (중복 refresh 방지)
|
|
113
|
+
export const CLAUDE_REFRESH_LOCK_PATH = join(
|
|
114
|
+
homedir(),
|
|
115
|
+
".claude",
|
|
116
|
+
"cache",
|
|
117
|
+
".claude-refresh-lock",
|
|
118
|
+
);
|
|
119
|
+
export const CODEX_REFRESH_LOCK_PATH = join(
|
|
120
|
+
homedir(),
|
|
121
|
+
".claude",
|
|
122
|
+
"cache",
|
|
123
|
+
".codex-refresh-lock",
|
|
124
|
+
);
|
|
125
|
+
export const GEMINI_QUOTA_REFRESH_LOCK_PATH = join(
|
|
126
|
+
homedir(),
|
|
127
|
+
".claude",
|
|
128
|
+
"cache",
|
|
129
|
+
".gemini-quota-refresh-lock",
|
|
130
|
+
);
|
|
131
|
+
export const GEMINI_SESSION_REFRESH_LOCK_PATH = join(
|
|
132
|
+
homedir(),
|
|
133
|
+
".claude",
|
|
134
|
+
"cache",
|
|
135
|
+
".gemini-session-refresh-lock",
|
|
136
|
+
);
|
|
137
|
+
export const SPAWN_LOCK_TTL_MS = 30 * 1000; // 30초 spawn dedup
|
|
138
|
+
|
|
45
139
|
// Gemini 쿼터 API 관련
|
|
46
140
|
export const GEMINI_OAUTH_PATH = join(homedir(), ".gemini", "oauth_creds.json");
|
|
47
|
-
export const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
141
|
+
export const ANTIGRAVITY_OAUTH_PATHS = [
|
|
142
|
+
join(homedir(), ".gemini", "antigravity-cli", "oauth_creds.json"),
|
|
143
|
+
join(homedir(), ".gemini", "antigravity-cli", "credentials.json"),
|
|
144
|
+
GEMINI_OAUTH_PATH,
|
|
145
|
+
];
|
|
146
|
+
export const GEMINI_QUOTA_CACHE_PATH = join(
|
|
147
|
+
homedir(),
|
|
148
|
+
".claude",
|
|
149
|
+
"cache",
|
|
150
|
+
"gemini-quota-cache.json",
|
|
151
|
+
);
|
|
152
|
+
export const GEMINI_PROJECT_CACHE_PATH = join(
|
|
153
|
+
homedir(),
|
|
154
|
+
".claude",
|
|
155
|
+
"cache",
|
|
156
|
+
"gemini-project-id.json",
|
|
157
|
+
);
|
|
158
|
+
export const GEMINI_SESSION_CACHE_PATH = join(
|
|
159
|
+
homedir(),
|
|
160
|
+
".claude",
|
|
161
|
+
"cache",
|
|
162
|
+
"gemini-session-cache.json",
|
|
163
|
+
);
|
|
164
|
+
export const TFX_PREFLIGHT_CACHE_PATH = join(
|
|
165
|
+
homedir(),
|
|
166
|
+
".claude",
|
|
167
|
+
"cache",
|
|
168
|
+
"tfx-preflight.json",
|
|
169
|
+
);
|
|
170
|
+
export const TFX_PREFLIGHT_CACHE_STALE_MS = 60 * 60 * 1000;
|
|
171
|
+
export const GEMINI_RPM_TRACKER_PATH = join(
|
|
172
|
+
homedir(),
|
|
173
|
+
".claude",
|
|
174
|
+
"cache",
|
|
175
|
+
"gemini-rpm-tracker.json",
|
|
176
|
+
);
|
|
177
|
+
export const SV_ACCUMULATOR_PATH = join(
|
|
178
|
+
homedir(),
|
|
179
|
+
".claude",
|
|
180
|
+
"cache",
|
|
181
|
+
"sv-accumulator.json",
|
|
182
|
+
);
|
|
52
183
|
// 이전 .omc/ 경로 fallback (기존 환경 호환)
|
|
53
|
-
export const LEGACY_GEMINI_QUOTA_CACHE = join(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
184
|
+
export const LEGACY_GEMINI_QUOTA_CACHE = join(
|
|
185
|
+
homedir(),
|
|
186
|
+
".omc",
|
|
187
|
+
"state",
|
|
188
|
+
"gemini_quota_cache.json",
|
|
189
|
+
);
|
|
190
|
+
export const LEGACY_GEMINI_PROJECT_CACHE = join(
|
|
191
|
+
homedir(),
|
|
192
|
+
".omc",
|
|
193
|
+
"state",
|
|
194
|
+
"gemini_project_id.json",
|
|
195
|
+
);
|
|
196
|
+
export const LEGACY_GEMINI_SESSION_CACHE = join(
|
|
197
|
+
homedir(),
|
|
198
|
+
".omc",
|
|
199
|
+
"state",
|
|
200
|
+
"gemini_session_tokens_cache.json",
|
|
201
|
+
);
|
|
202
|
+
export const LEGACY_GEMINI_RPM_TRACKER = join(
|
|
203
|
+
homedir(),
|
|
204
|
+
".omc",
|
|
205
|
+
"state",
|
|
206
|
+
"gemini_rpm_tracker.json",
|
|
207
|
+
);
|
|
208
|
+
export const LEGACY_SV_ACCUMULATOR = join(
|
|
209
|
+
homedir(),
|
|
210
|
+
".omc",
|
|
211
|
+
"state",
|
|
212
|
+
"sv-accumulator.json",
|
|
213
|
+
);
|
|
58
214
|
|
|
59
215
|
export const GEMINI_RPM_WINDOW_MS = 60 * 1000; // 60초 슬라이딩 윈도우
|
|
60
216
|
export const GEMINI_QUOTA_STALE_MS = 5 * 60 * 1000; // 5분
|
|
61
|
-
export const GEMINI_SESSION_STALE_MS =
|
|
217
|
+
export const GEMINI_SESSION_STALE_MS = 30 * 1000; // 30초
|
|
62
218
|
export const GEMINI_API_TIMEOUT_MS = 3000; // 3초
|
|
63
219
|
|
|
64
220
|
export const ACCOUNT_LABEL_WIDTH = 10;
|
|
@@ -84,5 +240,12 @@ export const ROWS_BUDGET_MEDIUM = 28;
|
|
|
84
240
|
export const ROWS_BUDGET_SMALL = 22;
|
|
85
241
|
|
|
86
242
|
// Gemini Pro 풀 공유 그룹: 같은 remainingFraction을 공유하는 모델 ID들
|
|
87
|
-
export const GEMINI_PRO_POOL = new Set([
|
|
88
|
-
|
|
243
|
+
export const GEMINI_PRO_POOL = new Set([
|
|
244
|
+
"gemini-2.5-pro",
|
|
245
|
+
"gemini-3-pro-preview",
|
|
246
|
+
"gemini-3.1-pro-preview",
|
|
247
|
+
]);
|
|
248
|
+
export const GEMINI_FLASH_POOL = new Set([
|
|
249
|
+
"gemini-2.5-flash",
|
|
250
|
+
"gemini-3-flash-preview",
|
|
251
|
+
]);
|
package/hud/context-monitor.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
4
|
|
|
6
5
|
import {
|
|
7
6
|
CONTEXT_MONITOR_CACHE_PATH,
|
|
@@ -11,9 +10,40 @@ import {
|
|
|
11
10
|
import { clampPercent, formatTokenCount, readJsonMigrate } from "./utils.mjs";
|
|
12
11
|
|
|
13
12
|
const DEFAULT_CONTEXT_LIMIT = 200_000;
|
|
13
|
+
const MILLION_CONTEXT_LIMIT = 1_000_000;
|
|
14
14
|
const MAX_CAPTURE_BYTES = 256 * 1024;
|
|
15
15
|
const MAX_TOP_KEYS = 20;
|
|
16
16
|
|
|
17
|
+
// stdin 이 context_window_size 를 제공하지 않을 때 모델 ID 로 한도를 추정한다.
|
|
18
|
+
// Anthropic 공식 문서(2026-04 기준): Opus 4.7 / Opus 4.6 / Sonnet 4.6 = 1M,
|
|
19
|
+
// Sonnet 4.5 / Haiku 4.5 = 200K. 그 외 모델은 [1m] suffix 로 opt-in 가능.
|
|
20
|
+
const MODEL_HINT_1M_PREFIXES = [
|
|
21
|
+
"claude-opus-4-7",
|
|
22
|
+
"claude-opus-4-6",
|
|
23
|
+
"claude-sonnet-4-6",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function normalizeModelId(modelId) {
|
|
27
|
+
if (!modelId) return "";
|
|
28
|
+
return String(modelId).toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveModelLimit(modelId) {
|
|
32
|
+
const id = normalizeModelId(modelId);
|
|
33
|
+
if (!id) return DEFAULT_CONTEXT_LIMIT;
|
|
34
|
+
if (id.includes("[1m]")) return MILLION_CONTEXT_LIMIT;
|
|
35
|
+
for (const prefix of MODEL_HINT_1M_PREFIXES) {
|
|
36
|
+
if (id.startsWith(prefix)) return MILLION_CONTEXT_LIMIT;
|
|
37
|
+
}
|
|
38
|
+
return DEFAULT_CONTEXT_LIMIT;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function shouldSuppressInfoOnlyContextStatus(modelId) {
|
|
42
|
+
const id = normalizeModelId(modelId);
|
|
43
|
+
if (!id) return false;
|
|
44
|
+
return id.startsWith("claude-opus-4-7") || id.includes("[1m]");
|
|
45
|
+
}
|
|
46
|
+
|
|
17
47
|
const WARNING_LEVELS = Object.freeze({
|
|
18
48
|
ok: { min: 0, message: "" },
|
|
19
49
|
info: { min: 60, message: "컨텍스트 절반 이상 사용" },
|
|
@@ -72,21 +102,24 @@ function normalizeUsage(usage) {
|
|
|
72
102
|
const input = Number(usage.input_tokens ?? usage.inputTokens ?? 0);
|
|
73
103
|
const output = Number(usage.output_tokens ?? usage.outputTokens ?? 0);
|
|
74
104
|
const cacheCreation = Number(
|
|
75
|
-
usage.cache_creation_input_tokens
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
105
|
+
usage.cache_creation_input_tokens ??
|
|
106
|
+
usage.cacheCreationInputTokens ??
|
|
107
|
+
usage.cache_creation_tokens ??
|
|
108
|
+
0,
|
|
79
109
|
);
|
|
80
110
|
const cacheRead = Number(
|
|
81
|
-
usage.cache_read_input_tokens
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
111
|
+
usage.cache_read_input_tokens ??
|
|
112
|
+
usage.cacheReadInputTokens ??
|
|
113
|
+
usage.cache_read_tokens ??
|
|
114
|
+
0,
|
|
85
115
|
);
|
|
86
|
-
const totalCandidate = Number(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
const totalCandidate = Number(
|
|
117
|
+
usage.total_tokens ?? usage.totalTokens ?? Number.NaN,
|
|
118
|
+
);
|
|
119
|
+
const total =
|
|
120
|
+
Number.isFinite(totalCandidate) && totalCandidate > 0
|
|
121
|
+
? totalCandidate
|
|
122
|
+
: input + output + cacheCreation + cacheRead;
|
|
90
123
|
if (!Number.isFinite(total) || total <= 0) return null;
|
|
91
124
|
return {
|
|
92
125
|
input: Math.max(0, Math.round(input)),
|
|
@@ -159,7 +192,10 @@ function extractFileKeys(args) {
|
|
|
159
192
|
function detectSkillHints(payloadText) {
|
|
160
193
|
if (!payloadText) return [];
|
|
161
194
|
const matches = payloadText.match(/\$[a-z0-9_-]+/gi) || [];
|
|
162
|
-
return Array.from(new Set(matches.map((m) => m.replace(/^\$/, "")))).slice(
|
|
195
|
+
return Array.from(new Set(matches.map((m) => m.replace(/^\$/, "")))).slice(
|
|
196
|
+
0,
|
|
197
|
+
5,
|
|
198
|
+
);
|
|
163
199
|
}
|
|
164
200
|
|
|
165
201
|
export function estimateTokens(input) {
|
|
@@ -172,21 +208,34 @@ export function parseUsageFromPayload(payload) {
|
|
|
172
208
|
|
|
173
209
|
export function classifyContextThreshold(percent) {
|
|
174
210
|
const p = clampThresholdPercent(percent);
|
|
175
|
-
if (p >= WARNING_LEVELS.critical.min)
|
|
176
|
-
|
|
177
|
-
if (p >= WARNING_LEVELS.
|
|
211
|
+
if (p >= WARNING_LEVELS.critical.min)
|
|
212
|
+
return { level: "critical", message: WARNING_LEVELS.critical.message };
|
|
213
|
+
if (p >= WARNING_LEVELS.warn.min)
|
|
214
|
+
return { level: "warn", message: WARNING_LEVELS.warn.message };
|
|
215
|
+
if (p >= WARNING_LEVELS.info.min)
|
|
216
|
+
return { level: "info", message: WARNING_LEVELS.info.message };
|
|
178
217
|
return { level: "ok", message: "" };
|
|
179
218
|
}
|
|
180
219
|
|
|
181
220
|
export function formatContextUsage(usedTokens, limitTokens, percent = null) {
|
|
182
221
|
const used = Math.max(0, Math.round(Number(usedTokens) || 0));
|
|
183
|
-
const limit = Math.max(
|
|
184
|
-
|
|
222
|
+
const limit = Math.max(
|
|
223
|
+
1,
|
|
224
|
+
Math.round(Number(limitTokens) || DEFAULT_CONTEXT_LIMIT),
|
|
225
|
+
);
|
|
226
|
+
const pct =
|
|
227
|
+
percent == null
|
|
228
|
+
? clampPercent((used / limit) * 100)
|
|
229
|
+
: clampPercent(percent);
|
|
185
230
|
return `${formatTokenCount(used)}/${formatTokenCount(limit)} (${pct}%)`;
|
|
186
231
|
}
|
|
187
232
|
|
|
188
233
|
export function readContextMonitorSnapshot() {
|
|
189
|
-
return readJsonMigrate(
|
|
234
|
+
return readJsonMigrate(
|
|
235
|
+
CONTEXT_MONITOR_CACHE_PATH,
|
|
236
|
+
CONTEXT_MONITOR_LEGACY_PATH,
|
|
237
|
+
null,
|
|
238
|
+
);
|
|
190
239
|
}
|
|
191
240
|
|
|
192
241
|
function getStdinContextUsage(stdin) {
|
|
@@ -194,9 +243,10 @@ function getStdinContextUsage(stdin) {
|
|
|
194
243
|
const nativePercent = Number(stdin?.context_window?.used_percentage);
|
|
195
244
|
const usage = stdin?.context_window?.current_usage || {};
|
|
196
245
|
const explicitUsed = Number(usage.total_tokens || 0);
|
|
197
|
-
const calculatedUsed =
|
|
198
|
-
|
|
199
|
-
|
|
246
|
+
const calculatedUsed =
|
|
247
|
+
Number(usage.input_tokens || 0) +
|
|
248
|
+
Number(usage.cache_creation_input_tokens || 0) +
|
|
249
|
+
Number(usage.cache_read_input_tokens || 0);
|
|
200
250
|
const usedTokens = explicitUsed > 0 ? explicitUsed : calculatedUsed;
|
|
201
251
|
|
|
202
252
|
if (limitTokens > 0 && usedTokens > 0) {
|
|
@@ -220,30 +270,50 @@ function getStdinContextUsage(stdin) {
|
|
|
220
270
|
return null;
|
|
221
271
|
}
|
|
222
272
|
|
|
273
|
+
export function deriveContextLimit(stdin) {
|
|
274
|
+
const explicit = Number(stdin?.context_window?.context_window_size || 0);
|
|
275
|
+
const modelHint = resolveModelLimit(stdin?.model?.id ?? stdin?.model);
|
|
276
|
+
return Math.max(explicit, modelHint);
|
|
277
|
+
}
|
|
278
|
+
|
|
223
279
|
export function buildContextUsageView(stdin, snapshot = null) {
|
|
224
280
|
const stdinUsage = getStdinContextUsage(stdin);
|
|
225
281
|
const monitor = snapshot || readContextMonitorSnapshot();
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
const limitTokens =
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
282
|
+
const modelId = stdin?.model?.id ?? stdin?.model;
|
|
283
|
+
const modelHintLimit = resolveModelLimit(modelId);
|
|
284
|
+
const monitorLimit = Number(monitor?.limitTokens || 0);
|
|
285
|
+
const stdinLimit = stdinUsage?.limitTokens;
|
|
286
|
+
const limitTokens =
|
|
287
|
+
stdinLimit != null && stdinLimit > 0
|
|
288
|
+
? Math.max(1, stdinLimit)
|
|
289
|
+
: modelId
|
|
290
|
+
? Math.max(1, monitorLimit, modelHintLimit)
|
|
291
|
+
: Math.max(1, monitorLimit || modelHintLimit);
|
|
292
|
+
|
|
293
|
+
const usedTokens = stdinUsage?.usedTokens ?? Number(monitor?.usedTokens || 0);
|
|
294
|
+
const percent =
|
|
295
|
+
limitTokens > 0 ? clampPercent((usedTokens / limitTokens) * 100) : 0;
|
|
234
296
|
|
|
235
297
|
const warning = classifyContextThreshold(percent);
|
|
298
|
+
const showInfoOnlyStatus = !(
|
|
299
|
+
warning.level === "info" && shouldSuppressInfoOnlyContextStatus(modelId)
|
|
300
|
+
);
|
|
236
301
|
return {
|
|
237
302
|
usedTokens,
|
|
238
303
|
limitTokens,
|
|
239
304
|
percent,
|
|
240
305
|
display: formatContextUsage(usedTokens, limitTokens, percent),
|
|
241
306
|
warningLevel: warning.level,
|
|
242
|
-
warningMessage: warning.message,
|
|
243
|
-
warningTag:
|
|
244
|
-
|
|
245
|
-
: warning.level === "
|
|
246
|
-
|
|
307
|
+
warningMessage: showInfoOnlyStatus ? warning.message : "",
|
|
308
|
+
warningTag: !showInfoOnlyStatus
|
|
309
|
+
? ""
|
|
310
|
+
: warning.level === "warn"
|
|
311
|
+
? "⚠ 압축 권장"
|
|
312
|
+
: warning.level === "critical"
|
|
313
|
+
? "‼ 분할 권장"
|
|
314
|
+
: warning.level === "info"
|
|
315
|
+
? "ℹ 절반 이상 사용"
|
|
316
|
+
: "",
|
|
247
317
|
source: stdinUsage?.source || (monitor ? "monitor" : "none"),
|
|
248
318
|
};
|
|
249
319
|
}
|
|
@@ -295,7 +365,10 @@ export function createContextMonitor(options = {}) {
|
|
|
295
365
|
const percent = clampPercent((state.usedTokens / state.limitTokens) * 100);
|
|
296
366
|
const warning = classifyContextThreshold(percent);
|
|
297
367
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
298
|
-
const reportPath = join(
|
|
368
|
+
const reportPath = join(
|
|
369
|
+
logsDir,
|
|
370
|
+
`context-usage-${state.sessionId}-${ts}.json`,
|
|
371
|
+
);
|
|
299
372
|
safeWriteJson(reportPath, {
|
|
300
373
|
sessionId: state.sessionId,
|
|
301
374
|
reason,
|
|
@@ -329,17 +402,31 @@ export function createContextMonitor(options = {}) {
|
|
|
329
402
|
toolName = "",
|
|
330
403
|
} = {}) => {
|
|
331
404
|
const started = process.hrtime.bigint();
|
|
332
|
-
const reqObj =
|
|
333
|
-
|
|
405
|
+
const reqObj =
|
|
406
|
+
typeof requestBody === "object"
|
|
407
|
+
? requestBody
|
|
408
|
+
: safeJsonParse(String(requestBody || ""));
|
|
409
|
+
const resObj =
|
|
410
|
+
typeof responseBody === "object"
|
|
411
|
+
? responseBody
|
|
412
|
+
: safeJsonParse(String(responseBody || ""));
|
|
334
413
|
|
|
335
414
|
const usage = parseUsageFromPayload(resObj);
|
|
336
|
-
const requestTokens =
|
|
337
|
-
|
|
415
|
+
const requestTokens =
|
|
416
|
+
requestBytes > 0
|
|
417
|
+
? toTokenEstimate(requestBytes)
|
|
418
|
+
: toTokenEstimate(requestBody);
|
|
419
|
+
const responseTokens =
|
|
420
|
+
usage?.total ??
|
|
421
|
+
(responseBytes > 0
|
|
422
|
+
? toTokenEstimate(responseBytes)
|
|
423
|
+
: toTokenEstimate(responseBody));
|
|
338
424
|
const totalTokens = Math.max(0, requestTokens + responseTokens);
|
|
339
425
|
|
|
340
426
|
const method = reqObj?.method || reqObj?.params?.name || "";
|
|
341
427
|
const name = toolName || reqObj?.params?.name || reqObj?.tool || "";
|
|
342
|
-
const args =
|
|
428
|
+
const args =
|
|
429
|
+
reqObj?.params?.arguments || reqObj?.arguments || reqObj?.params || {};
|
|
343
430
|
const payloadText = normalizeText(requestBody).slice(0, MAX_CAPTURE_BYTES);
|
|
344
431
|
const skills = detectSkillHints(payloadText);
|
|
345
432
|
const files = extractFileKeys(args);
|
|
@@ -383,7 +470,9 @@ export function createContextMonitor(options = {}) {
|
|
|
383
470
|
|
|
384
471
|
if (registerExitHooks) {
|
|
385
472
|
const flushOnExit = () => {
|
|
386
|
-
try {
|
|
473
|
+
try {
|
|
474
|
+
flush("process.exit");
|
|
475
|
+
} catch {}
|
|
387
476
|
};
|
|
388
477
|
process.once("exit", flushOnExit);
|
|
389
478
|
process.once("SIGINT", flushOnExit);
|