@triflux/core 10.1.0 → 10.25.1
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 +183 -23
- 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 +254 -33
- 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 +831 -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 +19 -23
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/service.mjs +88 -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 +77 -95
- 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 +76 -28
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +22 -16
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +184 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +153 -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 +184 -51
- package/hud/renderers.mjs +413 -123
- 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 +67 -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 +48 -19
- 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 +70 -10
- 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
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);
|
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -5,46 +5,78 @@
|
|
|
5
5
|
// 각 모듈에서 색상, 터미널, 프로바이더, 렌더러를 가져와 조합한다.
|
|
6
6
|
// ============================================================================
|
|
7
7
|
|
|
8
|
-
import { DIM, RESET, bold, dim, green, claudeOrange, codexWhite, geminiBlue, colorByPercent } from "./colors.mjs";
|
|
9
8
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
bold,
|
|
10
|
+
claudeOrange,
|
|
11
|
+
codexWhite,
|
|
12
|
+
DIM,
|
|
13
|
+
dim,
|
|
14
|
+
geminiBlue,
|
|
15
|
+
green,
|
|
16
|
+
RESET,
|
|
17
|
+
} from "./colors.mjs";
|
|
18
|
+
import {
|
|
19
|
+
ACCOUNTS_CONFIG_PATH,
|
|
20
|
+
ACCOUNTS_STATE_PATH,
|
|
21
|
+
CLAUDE_REFRESH_FLAG,
|
|
22
|
+
CODEX_REFRESH_FLAG,
|
|
23
|
+
GEMINI_FLASH_POOL,
|
|
24
|
+
GEMINI_PRO_POOL,
|
|
25
|
+
GEMINI_REFRESH_FLAG,
|
|
26
|
+
GEMINI_SESSION_REFRESH_FLAG,
|
|
27
|
+
QOS_PATH,
|
|
28
|
+
TFX_PREFLIGHT_CACHE_PATH,
|
|
14
29
|
} from "./constants.mjs";
|
|
15
30
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
buildContextUsageView,
|
|
32
|
+
deriveContextLimit,
|
|
33
|
+
} from "./context-monitor.mjs";
|
|
34
|
+
import { getMissionBoardState } from "./mission-board.mjs";
|
|
20
35
|
// Claude provider
|
|
21
36
|
import {
|
|
22
|
-
|
|
37
|
+
fetchClaudeUsage,
|
|
23
38
|
readClaudeContextSnapshot,
|
|
39
|
+
readClaudeUsageSnapshot,
|
|
40
|
+
scheduleClaudeUsageRefresh,
|
|
24
41
|
} from "./providers/claude.mjs";
|
|
25
|
-
import { buildContextUsageView } from "./context-monitor.mjs";
|
|
26
|
-
|
|
27
42
|
// Codex provider
|
|
28
43
|
import {
|
|
29
|
-
getCodexEmail,
|
|
30
|
-
|
|
44
|
+
getCodexEmail,
|
|
45
|
+
readCodexRateLimitSnapshot,
|
|
46
|
+
refreshCodexRateLimitsCache,
|
|
47
|
+
scheduleCodexRateLimitRefresh,
|
|
31
48
|
} from "./providers/codex.mjs";
|
|
32
|
-
|
|
33
49
|
// Gemini provider
|
|
34
50
|
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
51
|
+
buildGeminiAuthContext,
|
|
52
|
+
fetchGeminiQuota,
|
|
53
|
+
getGeminiEmail,
|
|
54
|
+
readGeminiQuotaSnapshot,
|
|
55
|
+
readGeminiSessionSnapshot,
|
|
56
|
+
refreshGeminiSessionCache,
|
|
57
|
+
scheduleGeminiQuotaRefresh,
|
|
58
|
+
scheduleGeminiSessionRefresh,
|
|
39
59
|
} from "./providers/gemini.mjs";
|
|
40
|
-
|
|
41
60
|
// Renderers
|
|
42
61
|
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
formatTokenSummary,
|
|
63
|
+
getClaudeRows,
|
|
64
|
+
getMicroLine,
|
|
65
|
+
getProviderRow,
|
|
66
|
+
getTeamRow,
|
|
67
|
+
readLatestBenchmarkDiff,
|
|
68
|
+
readSvAccumulator,
|
|
69
|
+
readTokenSavings,
|
|
70
|
+
renderAlignedRows,
|
|
71
|
+
renderMissionBoard,
|
|
47
72
|
} from "./renderers.mjs";
|
|
73
|
+
import { selectTier } from "./terminal.mjs";
|
|
74
|
+
import {
|
|
75
|
+
getCliArgValue,
|
|
76
|
+
getProviderAccountId,
|
|
77
|
+
readJson,
|
|
78
|
+
readStdinJson,
|
|
79
|
+
} from "./utils.mjs";
|
|
48
80
|
|
|
49
81
|
// ============================================================================
|
|
50
82
|
// 메인
|
|
@@ -78,6 +110,11 @@ async function main() {
|
|
|
78
110
|
const stdinPromise = readStdinJson();
|
|
79
111
|
|
|
80
112
|
const qosProfile = readJson(QOS_PATH, { providers: {} });
|
|
113
|
+
const preflightCache = readJson(TFX_PREFLIGHT_CACHE_PATH, null);
|
|
114
|
+
const antigravityReady =
|
|
115
|
+
process.env.TFX_ANTIGRAVITY_OK === "1" ||
|
|
116
|
+
preflightCache?.antigravity?.ok === true ||
|
|
117
|
+
preflightCache?.available_agents?.includes("antigravity");
|
|
81
118
|
const accountsConfig = readJson(ACCOUNTS_CONFIG_PATH, { providers: {} });
|
|
82
119
|
const accountsState = readJson(ACCOUNTS_STATE_PATH, { providers: {} });
|
|
83
120
|
const claudeUsageSnapshot = readClaudeUsageSnapshot();
|
|
@@ -85,11 +122,18 @@ async function main() {
|
|
|
85
122
|
if (claudeUsageSnapshot.shouldRefresh) {
|
|
86
123
|
scheduleClaudeUsageRefresh();
|
|
87
124
|
}
|
|
88
|
-
const geminiAccountId = getProviderAccountId(
|
|
125
|
+
const geminiAccountId = getProviderAccountId(
|
|
126
|
+
"gemini",
|
|
127
|
+
accountsConfig,
|
|
128
|
+
accountsState,
|
|
129
|
+
);
|
|
89
130
|
const codexSnapshot = readCodexRateLimitSnapshot();
|
|
90
131
|
const geminiSessionSnapshot = readGeminiSessionSnapshot();
|
|
91
132
|
const geminiAuthContext = buildGeminiAuthContext(geminiAccountId);
|
|
92
|
-
const geminiQuotaSnapshot = readGeminiQuotaSnapshot(
|
|
133
|
+
const geminiQuotaSnapshot = readGeminiQuotaSnapshot(
|
|
134
|
+
geminiAccountId,
|
|
135
|
+
geminiAuthContext,
|
|
136
|
+
);
|
|
93
137
|
if (codexSnapshot.shouldRefresh) {
|
|
94
138
|
scheduleCodexRateLimitRefresh();
|
|
95
139
|
}
|
|
@@ -103,25 +147,30 @@ async function main() {
|
|
|
103
147
|
// 실측 데이터 추출
|
|
104
148
|
const stdin = await stdinPromise;
|
|
105
149
|
const contextView = buildContextUsageView(stdin, contextSnapshot);
|
|
150
|
+
const claudeUsage = claudeUsageSnapshot.data;
|
|
106
151
|
const codexEmail = getCodexEmail();
|
|
107
152
|
const geminiEmail = getGeminiEmail();
|
|
108
153
|
const codexBuckets = codexSnapshot.buckets;
|
|
109
154
|
const geminiSession = geminiSessionSnapshot.session;
|
|
110
155
|
const geminiQuota = geminiQuotaSnapshot.quota;
|
|
156
|
+
const missionBoardState = await getMissionBoardState();
|
|
111
157
|
|
|
112
158
|
// 누적 절약 데이터 읽기
|
|
113
159
|
const svSavings = readTokenSavings();
|
|
114
160
|
const svAccumulator = readSvAccumulator();
|
|
115
|
-
const
|
|
161
|
+
const _totalCostSaved =
|
|
162
|
+
svSavings?.totalSaved || svAccumulator?.totalCostSaved || 0;
|
|
116
163
|
|
|
117
164
|
// 세션/누적 토큰 → context 대비 절약 배수 (개별 provider sv%)
|
|
118
|
-
const ctxCapacity = stdin
|
|
165
|
+
const ctxCapacity = deriveContextLimit(stdin);
|
|
119
166
|
let codexSv = null;
|
|
120
167
|
if (svAccumulator?.codex?.tokens > 0) {
|
|
121
168
|
codexSv = svAccumulator.codex.tokens / ctxCapacity;
|
|
122
169
|
} else if (codexBuckets) {
|
|
123
|
-
const main =
|
|
124
|
-
|
|
170
|
+
const main =
|
|
171
|
+
codexBuckets.codex || codexBuckets[Object.keys(codexBuckets)[0]];
|
|
172
|
+
if (main?.tokens?.total_tokens)
|
|
173
|
+
codexSv = main.tokens.total_tokens / ctxCapacity;
|
|
125
174
|
}
|
|
126
175
|
let geminiSv = null;
|
|
127
176
|
if (svAccumulator?.gemini?.tokens > 0) {
|
|
@@ -134,12 +183,16 @@ async function main() {
|
|
|
134
183
|
// Gemini: 3풀 버킷 추출 (Pro/Flash/Lite — 각 풀 내 모델들은 쿼터 공유)
|
|
135
184
|
const geminiModel = geminiSession?.model || "gemini-3-flash-preview";
|
|
136
185
|
const geminiBuckets = geminiQuota?.buckets || [];
|
|
137
|
-
const geminiBucket =
|
|
138
|
-
|
|
139
|
-
||
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
186
|
+
const geminiBucket =
|
|
187
|
+
geminiBuckets.find((b) => b.modelId === geminiModel) ||
|
|
188
|
+
geminiBuckets.find((b) => b.modelId === "gemini-3-flash-preview") ||
|
|
189
|
+
null;
|
|
190
|
+
const geminiProBucket =
|
|
191
|
+
geminiBuckets.find((b) => GEMINI_PRO_POOL.has(b.modelId)) || null;
|
|
192
|
+
const geminiFlashBucket =
|
|
193
|
+
geminiBuckets.find((b) => GEMINI_FLASH_POOL.has(b.modelId)) || null;
|
|
194
|
+
const geminiLiteBucket =
|
|
195
|
+
geminiBuckets.find((b) => b.modelId?.includes("flash-lite")) || null;
|
|
143
196
|
|
|
144
197
|
// 합산 절약: Codex+Gemini sv% 합산 (컨텍스트 대비 위임 토큰 비율)
|
|
145
198
|
const combinedSvPct = Math.round(((codexSv ?? 0) + (geminiSv ?? 0)) * 100);
|
|
@@ -149,32 +202,76 @@ async function main() {
|
|
|
149
202
|
|
|
150
203
|
// nano tier: 1줄 모드 (극소 폭 또는 알림 배너 대응)
|
|
151
204
|
if (CURRENT_TIER === "nano") {
|
|
152
|
-
const microLine = getMicroLine(
|
|
153
|
-
|
|
205
|
+
const microLine = getMicroLine(
|
|
206
|
+
contextView,
|
|
207
|
+
claudeUsage,
|
|
208
|
+
codexBuckets,
|
|
209
|
+
geminiSession,
|
|
210
|
+
geminiBucket,
|
|
211
|
+
combinedSvPct,
|
|
212
|
+
antigravityReady ? "a" : "g",
|
|
213
|
+
);
|
|
154
214
|
process.stdout.write(`\x1b[0m${microLine}\n`);
|
|
155
215
|
return;
|
|
156
216
|
}
|
|
157
217
|
|
|
158
|
-
const codexQuotaData = codexBuckets
|
|
218
|
+
const codexQuotaData = codexBuckets
|
|
219
|
+
? { type: "codex", buckets: codexBuckets }
|
|
220
|
+
: null;
|
|
159
221
|
const geminiQuotaData = {
|
|
160
222
|
type: "gemini",
|
|
161
223
|
quotaBucket: geminiBucket,
|
|
162
|
-
pools: {
|
|
224
|
+
pools: {
|
|
225
|
+
pro: geminiProBucket,
|
|
226
|
+
flash: geminiFlashBucket,
|
|
227
|
+
lite: geminiLiteBucket,
|
|
228
|
+
},
|
|
163
229
|
session: geminiSession,
|
|
164
230
|
};
|
|
165
231
|
|
|
166
232
|
const rows = [
|
|
167
|
-
...getClaudeRows(CURRENT_TIER, contextView,
|
|
168
|
-
getProviderRow(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
233
|
+
...getClaudeRows(CURRENT_TIER, contextView, claudeUsage, combinedSvPct),
|
|
234
|
+
getProviderRow(
|
|
235
|
+
CURRENT_TIER,
|
|
236
|
+
"codex",
|
|
237
|
+
"x",
|
|
238
|
+
codexWhite,
|
|
239
|
+
qosProfile,
|
|
240
|
+
accountsConfig,
|
|
241
|
+
accountsState,
|
|
242
|
+
codexQuotaData,
|
|
243
|
+
codexEmail,
|
|
244
|
+
codexSv,
|
|
245
|
+
null,
|
|
246
|
+
),
|
|
247
|
+
getProviderRow(
|
|
248
|
+
CURRENT_TIER,
|
|
249
|
+
antigravityReady ? "antigravity" : "gemini",
|
|
250
|
+
antigravityReady ? "a" : "g",
|
|
251
|
+
geminiBlue,
|
|
252
|
+
qosProfile,
|
|
253
|
+
accountsConfig,
|
|
254
|
+
accountsState,
|
|
255
|
+
geminiQuotaData,
|
|
256
|
+
antigravityReady ? "anti" : geminiEmail,
|
|
257
|
+
geminiSv,
|
|
258
|
+
null,
|
|
259
|
+
),
|
|
172
260
|
];
|
|
173
261
|
|
|
174
262
|
// tfx-multi 활성 시 팀 상태 행 추가 (v2.2)
|
|
175
263
|
const teamRow = getTeamRow(CURRENT_TIER);
|
|
176
264
|
if (teamRow) rows.push(teamRow);
|
|
177
265
|
|
|
266
|
+
const missionBoard = renderMissionBoard(missionBoardState);
|
|
267
|
+
if (missionBoard) {
|
|
268
|
+
rows.push({
|
|
269
|
+
prefix: bold(claudeOrange("\u25B2")),
|
|
270
|
+
left: missionBoard,
|
|
271
|
+
right: "",
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
178
275
|
// 최근 벤치마크 diff → 토큰 요약 행 추가
|
|
179
276
|
const latestDiff = readLatestBenchmarkDiff();
|
|
180
277
|
if (latestDiff) {
|
|
@@ -186,10 +283,14 @@ async function main() {
|
|
|
186
283
|
|
|
187
284
|
// 비활성 프로바이더 dim 처리: 데이터 없으면 전체 줄 dim
|
|
188
285
|
const codexActive = codexBuckets != null;
|
|
189
|
-
const geminiActive =
|
|
190
|
-
|
|
286
|
+
const geminiActive =
|
|
287
|
+
antigravityReady ||
|
|
288
|
+
(geminiSession?.total || 0) > 0 ||
|
|
289
|
+
geminiBucket != null ||
|
|
290
|
+
geminiProBucket != null ||
|
|
291
|
+
geminiFlashBucket != null;
|
|
191
292
|
|
|
192
|
-
|
|
293
|
+
const outputLines = renderAlignedRows(rows);
|
|
193
294
|
|
|
194
295
|
// 비활성 줄 dim 래핑 (rows 순서: [claude, codex, gemini])
|
|
195
296
|
if (outputLines.length >= 3) {
|
|
@@ -201,10 +302,12 @@ async function main() {
|
|
|
201
302
|
const contextPercent = contextView.percent;
|
|
202
303
|
const leadingBreaks = contextPercent >= 85 ? "\n\n" : "\n";
|
|
203
304
|
// 줄별 RESET: Claude Code TUI 스타일 간섭 방지 (색상 밝기 버그 수정)
|
|
204
|
-
const resetedLines = outputLines.map(line => `\x1b[0m${line}`);
|
|
305
|
+
const resetedLines = outputLines.map((line) => `\x1b[0m${line}`);
|
|
205
306
|
process.stdout.write(`${leadingBreaks}${resetedLines.join("\n")}\n`);
|
|
206
307
|
}
|
|
207
308
|
|
|
208
309
|
main().catch(() => {
|
|
209
|
-
process.stdout.write(
|
|
310
|
+
process.stdout.write(
|
|
311
|
+
`\x1b[0m${bold(claudeOrange("c"))}: ${dim("5h:")}${green("0%")} ${dim("(n/a)")} ${dim("1w:")}${green("0%")} ${dim("(n/a)")} ${dim("|")} ${dim("ctx:")}${green("0%")}\n`,
|
|
312
|
+
);
|
|
210
313
|
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// HUD Mission Board — .omc/state/sessions/ 기반 에이전트 진행률 집계
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
const SESSIONS_DIR = join(homedir(), ".omc", "state", "sessions");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* .omc/state/sessions/ 디렉토리를 읽어 팀 상태를 반환한다.
|
|
12
|
+
* @returns {{ agents: Array<{name: string, status: string, progress: number}>, dagLevel: number, totalProgress: number } | null}
|
|
13
|
+
*/
|
|
14
|
+
export async function getMissionBoardState(sessionsDir = SESSIONS_DIR) {
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = await readdir(sessionsDir);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const jsonFiles = entries.filter((f) => f.endsWith(".json"));
|
|
23
|
+
if (jsonFiles.length === 0) return null;
|
|
24
|
+
|
|
25
|
+
const agents = [];
|
|
26
|
+
for (const file of jsonFiles) {
|
|
27
|
+
let data;
|
|
28
|
+
try {
|
|
29
|
+
const raw = await readFile(join(sessionsDir, file), "utf8");
|
|
30
|
+
data = JSON.parse(raw);
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const name = data.name ?? file.replace(/\.json$/, "");
|
|
36
|
+
const status = data.status ?? "idle";
|
|
37
|
+
const progress = typeof data.progress === "number" ? data.progress : 0;
|
|
38
|
+
agents.push({ name, status, progress });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (agents.length === 0) return null;
|
|
42
|
+
|
|
43
|
+
const totalProgress =
|
|
44
|
+
agents.length > 0
|
|
45
|
+
? Math.round(
|
|
46
|
+
agents.reduce((sum, a) => sum + a.progress, 0) / agents.length,
|
|
47
|
+
)
|
|
48
|
+
: 0;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
agents,
|
|
52
|
+
// TODO: derive dagLevel from real mission dependency metadata instead of hardcoding 0.
|
|
53
|
+
dagLevel: 0,
|
|
54
|
+
totalProgress,
|
|
55
|
+
};
|
|
56
|
+
}
|