@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.
Files changed (145) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +20 -8
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +267 -35
  7. package/hooks/hook-registry.json +343 -256
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +191 -31
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +262 -34
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +1051 -290
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -4
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +22 -21
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/schema/delegator-tools.schema.json +1 -1
  35. package/hub/delegator/service.mjs +93 -64
  36. package/hub/delegator/tool-definitions.mjs +35 -35
  37. package/hub/dynamic-routing-engine.mjs +511 -0
  38. package/hub/fullcycle.mjs +34 -18
  39. package/hub/gemini-adapter.mjs +82 -108
  40. package/hub/hitl.mjs +202 -143
  41. package/hub/intent.mjs +167 -38
  42. package/hub/lib/bash-path.mjs +73 -0
  43. package/hub/lib/cache-guard.mjs +43 -17
  44. package/hub/lib/env-detect.mjs +145 -0
  45. package/hub/lib/hosts-compat.mjs +319 -0
  46. package/hub/lib/mcp-response-cache.mjs +239 -0
  47. package/hub/lib/memory-store.mjs +285 -111
  48. package/hub/lib/path-utils.mjs +165 -0
  49. package/hub/lib/phase-manager.mjs +493 -0
  50. package/hub/lib/process-utils.mjs +797 -104
  51. package/hub/lib/prompt-tmp.mjs +33 -0
  52. package/hub/lib/risk-tier.mjs +53 -0
  53. package/hub/lib/spawn-trace.mjs +561 -0
  54. package/hub/lib/ssh-command.mjs +44 -24
  55. package/hub/lib/ssh-retry.mjs +5 -1
  56. package/hub/lib/staged-file-detect.mjs +43 -0
  57. package/hub/lib/state-snapshot.mjs +293 -0
  58. package/hub/lib/tfx-route-args.mjs +309 -0
  59. package/hub/lib/timeline-adapter.mjs +190 -0
  60. package/hub/lib/trace-recorder.mjs +153 -0
  61. package/hub/lib/uuidv7.mjs +4 -3
  62. package/hub/memory-doctor.mjs +266 -106
  63. package/hub/middleware/quota-middleware.mjs +70 -0
  64. package/hub/middleware/request-logger.mjs +61 -34
  65. package/hub/paths.mjs +9 -9
  66. package/hub/pipeline/gates/confidence.mjs +34 -15
  67. package/hub/pipeline/gates/consensus.mjs +27 -15
  68. package/hub/pipeline/gates/index.mjs +7 -3
  69. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  70. package/hub/pipeline/index.mjs +77 -42
  71. package/hub/pipeline/state.mjs +10 -10
  72. package/hub/pipeline/transitions.mjs +40 -23
  73. package/hub/platform.mjs +65 -48
  74. package/hub/quality/deslop.mjs +70 -49
  75. package/hub/reflexion.mjs +87 -13
  76. package/hub/research.mjs +33 -25
  77. package/hub/router.mjs +941 -791
  78. package/hub/routing/complexity.mjs +132 -29
  79. package/hub/routing/index.mjs +17 -12
  80. package/hub/routing/q-learning.mjs +78 -30
  81. package/hub/routing-snapshot.mjs +261 -0
  82. package/hub/session-fingerprint.mjs +127 -61
  83. package/hub/state.mjs +86 -44
  84. package/hub/team/claude-daemon-control.mjs +1130 -0
  85. package/hub/team/retry-state-machine.mjs +336 -0
  86. package/hub/token-mode.mjs +72 -71
  87. package/hub/workers/worker-utils.mjs +23 -17
  88. package/hud/colors.mjs +27 -9
  89. package/hud/constants.mjs +190 -27
  90. package/hud/context-monitor.mjs +133 -44
  91. package/hud/hud-qos-status.mjs +157 -50
  92. package/hud/mission-board.mjs +56 -0
  93. package/hud/providers/claude.mjs +398 -114
  94. package/hud/providers/codex.mjs +96 -21
  95. package/hud/providers/gemini.mjs +210 -52
  96. package/hud/renderers.mjs +440 -127
  97. package/hud/terminal.mjs +101 -31
  98. package/hud/utils.mjs +81 -39
  99. package/mesh/index.mjs +69 -0
  100. package/mesh/mesh-budget.mjs +137 -0
  101. package/mesh/mesh-heartbeat.mjs +100 -0
  102. package/mesh/mesh-protocol.mjs +96 -0
  103. package/mesh/mesh-queue.mjs +163 -0
  104. package/mesh/mesh-registry.mjs +78 -0
  105. package/mesh/mesh-router.mjs +77 -0
  106. package/package.json +5 -1
  107. package/scripts/lib/agent-json.mjs +27 -0
  108. package/scripts/lib/async.mjs +174 -0
  109. package/scripts/lib/claudemd-scanner.mjs +6 -1
  110. package/scripts/lib/cli-agy.mjs +62 -0
  111. package/scripts/lib/cli-claude.mjs +78 -0
  112. package/scripts/lib/cli-codex.mjs +199 -0
  113. package/scripts/lib/cli-gemini.mjs +37 -0
  114. package/scripts/lib/codex-recovery.sh +50 -0
  115. package/scripts/lib/context.mjs +3 -3
  116. package/scripts/lib/cross-review-utils.mjs +13 -5
  117. package/scripts/lib/doctor-env-checks.mjs +121 -0
  118. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  119. package/scripts/lib/env-probe.mjs +58 -30
  120. package/scripts/lib/env.mjs +14 -0
  121. package/scripts/lib/gemini-profiles.mjs +44 -10
  122. package/scripts/lib/handoff.mjs +33 -17
  123. package/scripts/lib/hook-utils.mjs +8 -6
  124. package/scripts/lib/hub.mjs +48 -0
  125. package/scripts/lib/keyword-rules.mjs +49 -20
  126. package/scripts/lib/logger.mjs +24 -24
  127. package/scripts/lib/mcp-filter.mjs +391 -247
  128. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  129. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  130. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  131. package/scripts/lib/mcp-guard-engine.mjs +1281 -115
  132. package/scripts/lib/mcp-health.mjs +773 -0
  133. package/scripts/lib/mcp-manifest.mjs +24 -14
  134. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  135. package/scripts/lib/pid.mjs +63 -0
  136. package/scripts/lib/process-utils.mjs +26 -0
  137. package/scripts/lib/psmux-info.mjs +117 -12
  138. package/scripts/lib/quota.mjs +47 -0
  139. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  140. package/scripts/lib/skill-state.mjs +220 -0
  141. package/scripts/lib/skill-template.mjs +32 -7
  142. package/scripts/lib/team.mjs +75 -0
  143. package/scripts/lib/timeout.mjs +66 -0
  144. package/scripts/lib/tmp.mjs +26 -0
  145. package/scripts/lib/toml.mjs +113 -0
@@ -1,14 +1,12 @@
1
1
  // hub/workers/worker-utils.mjs — 워커 공통 유틸리티
2
- // claude-worker, gemini-worker, pipe 등에서 공유하는 순수 유틸 함수 모음.
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
- const timer = setTimeout(resolve, Math.max(0, delayMs));
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 = Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs)
55
- * (0.5 + Math.random() * 0.5);
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 === 'string') {
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 !== 'object') return;
75
+ if (typeof value !== "object") return;
78
76
 
79
- if (typeof value.text === 'string') appendTextFragments(value.text, parts);
80
- if (typeof value.response === 'string') appendTextFragments(value.response, parts);
81
- if (typeof value.result === 'string') appendTextFragments(value.result, parts);
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('\n').trim();
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 { child.stdin.end(); } catch {}
96
- try { child.kill(); } catch {}
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 { child.kill('SIGKILL'); } catch {}
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) { return `${GREEN}${t}${RESET}`; }
16
- export function yellow(t) { return `${YELLOW}${t}${RESET}`; }
17
- export function red(t) { return `${RED}${t}${RESET}`; }
18
- export function cyan(t) { return `${CYAN}${t}${RESET}`; }
19
- export function dim(t) { return `${DIM}${t}${RESET}`; }
20
- export function bold(t) { return `${BOLD}${t}${RESET}`; }
21
- export function claudeOrange(t) { return `${CLAUDE_ORANGE}${t}${RESET}`; }
22
- export function codexWhite(t) { return `${CODEX_WHITE}${t}${RESET}`; }
23
- export function geminiBlue(t) { return `${GEMINI_BLUE}${t}${RESET}`; }
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(homedir(), ".omc", "state", "cli_qos_profile.json");
10
- export const ACCOUNTS_CONFIG_PATH = join(homedir(), ".omc", "router", "accounts.json");
11
- export const ACCOUNTS_STATE_PATH = join(homedir(), ".omc", "state", "cli_accounts_state.json");
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(homedir(), ".claude", "cache", "tfx-hub", "team-state.json");
15
- export const CONTEXT_MONITOR_CACHE_PATH = join(homedir(), ".claude", "cache", "tfx-hub", "context-monitor.json");
16
- export const CONTEXT_MONITOR_LEGACY_PATH = join(homedir(), ".omc", "state", "context-monitor.json");
17
- export const CONTEXT_MONITOR_LOG_DIR = join(homedir(), ".omc", "logs");
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(homedir(), ".claude", "cache", "tfx-hub", "remote-env");
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(homedir(), ".claude", ".credentials.json");
25
- export const CLAUDE_USAGE_CACHE_PATH = join(homedir(), ".claude", "cache", "claude-usage-cache.json");
26
- export const OMC_PLUGIN_USAGE_CACHE_PATH = join(homedir(), ".claude", "plugins", "oh-my-claudecode", ".usage-cache.json");
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분 (OMC가 30초마다 갱신)
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(homedir(), ".claude", "cache", "codex-rate-limits-cache.json");
42
- export const CODEX_QUOTA_STALE_MS = 15 * 1000; // 15초
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 GEMINI_QUOTA_CACHE_PATH = join(homedir(), ".claude", "cache", "gemini-quota-cache.json");
48
- export const GEMINI_PROJECT_CACHE_PATH = join(homedir(), ".claude", "cache", "gemini-project-id.json");
49
- export const GEMINI_SESSION_CACHE_PATH = join(homedir(), ".claude", "cache", "gemini-session-cache.json");
50
- export const GEMINI_RPM_TRACKER_PATH = join(homedir(), ".claude", "cache", "gemini-rpm-tracker.json");
51
- export const SV_ACCUMULATOR_PATH = join(homedir(), ".claude", "cache", "sv-accumulator.json");
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(homedir(), ".omc", "state", "gemini_quota_cache.json");
54
- export const LEGACY_GEMINI_PROJECT_CACHE = join(homedir(), ".omc", "state", "gemini_project_id.json");
55
- export const LEGACY_GEMINI_SESSION_CACHE = join(homedir(), ".omc", "state", "gemini_session_tokens_cache.json");
56
- export const LEGACY_GEMINI_RPM_TRACKER = join(homedir(), ".omc", "state", "gemini_rpm_tracker.json");
57
- export const LEGACY_SV_ACCUMULATOR = join(homedir(), ".omc", "state", "sv-accumulator.json");
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 = 15 * 1000; // 15초
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(["gemini-2.5-pro", "gemini-3-pro-preview", "gemini-3.1-pro-preview"]);
88
- export const GEMINI_FLASH_POOL = new Set(["gemini-2.5-flash", "gemini-3-flash-preview"]);
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
+ ]);
@@ -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
- ?? usage.cacheCreationInputTokens
77
- ?? usage.cache_creation_tokens
78
- ?? 0,
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
- ?? usage.cacheReadInputTokens
83
- ?? usage.cache_read_tokens
84
- ?? 0,
111
+ usage.cache_read_input_tokens ??
112
+ usage.cacheReadInputTokens ??
113
+ usage.cache_read_tokens ??
114
+ 0,
85
115
  );
86
- const totalCandidate = Number(usage.total_tokens ?? usage.totalTokens ?? Number.NaN);
87
- const total = Number.isFinite(totalCandidate) && totalCandidate > 0
88
- ? totalCandidate
89
- : input + output + cacheCreation + cacheRead;
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(0, 5);
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) return { level: "critical", message: WARNING_LEVELS.critical.message };
176
- if (p >= WARNING_LEVELS.warn.min) return { level: "warn", message: WARNING_LEVELS.warn.message };
177
- if (p >= WARNING_LEVELS.info.min) return { level: "info", message: WARNING_LEVELS.info.message };
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(1, Math.round(Number(limitTokens) || DEFAULT_CONTEXT_LIMIT));
184
- const pct = percent == null ? clampPercent((used / limit) * 100) : clampPercent(percent);
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(CONTEXT_MONITOR_CACHE_PATH, CONTEXT_MONITOR_LEGACY_PATH, null);
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 = Number(usage.input_tokens || 0)
198
- + Number(usage.cache_creation_input_tokens || 0)
199
- + Number(usage.cache_read_input_tokens || 0);
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 fallbackLimit = Number(monitor?.limitTokens || DEFAULT_CONTEXT_LIMIT);
227
-
228
- const usedTokens = stdinUsage?.usedTokens
229
- ?? Number(monitor?.usedTokens || 0);
230
- const limitTokens = stdinUsage?.limitTokens
231
- ?? Math.max(1, fallbackLimit);
232
- const percent = stdinUsage?.percent
233
- ?? (limitTokens > 0 ? clampPercent((usedTokens / limitTokens) * 100) : 0);
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: warning.level === "warn" ? "⚠ 압축 권장"
244
- : warning.level === "critical" ? "‼ 분할 권장"
245
- : warning.level === "info" ? "ℹ 절반 이상 사용"
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(logsDir, `context-usage-${state.sessionId}-${ts}.json`);
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 = typeof requestBody === "object" ? requestBody : safeJsonParse(String(requestBody || ""));
333
- const resObj = typeof responseBody === "object" ? responseBody : safeJsonParse(String(responseBody || ""));
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 = requestBytes > 0 ? toTokenEstimate(requestBytes) : toTokenEstimate(requestBody);
337
- const responseTokens = usage?.total ?? (responseBytes > 0 ? toTokenEstimate(responseBytes) : toTokenEstimate(responseBody));
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 = reqObj?.params?.arguments || reqObj?.arguments || reqObj?.params || {};
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 { flush("process.exit"); } catch {}
473
+ try {
474
+ flush("process.exit");
475
+ } catch {}
387
476
  };
388
477
  process.once("exit", flushOnExit);
389
478
  process.once("SIGINT", flushOnExit);