@triflux/core 10.49.0 → 10.50.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 (46) hide show
  1. package/hooks/agy-session-hook.mjs +1 -1
  2. package/hooks/claude-cwd-projection-refresh.mjs +7 -79
  3. package/hooks/codex-session-hook.mjs +3 -8
  4. package/hooks/hook-orchestrator.mjs +8 -648
  5. package/hooks/lib/resolve-root.mjs +1 -1
  6. package/hub/bridge.mjs +290 -51
  7. package/hub/team/claude-daemon-control.mjs +54 -6
  8. package/hub/team/claude-transcript.mjs +240 -0
  9. package/hub/team/session-context.mjs +203 -0
  10. package/hud/constants.mjs +10 -0
  11. package/hud/hud-qos-status.mjs +23 -4
  12. package/hud/providers/gemini.mjs +4 -1
  13. package/hud/renderers.mjs +10 -6
  14. package/package.json +1 -1
  15. package/scripts/lib/doctor-env-checks.mjs +0 -85
  16. package/scripts/lib/legacy-hook-cleanup.mjs +230 -0
  17. package/scripts/lib/legacy-mcp-cleanup.mjs +772 -0
  18. package/scripts/lib/mcp-guard-engine.mjs +44 -271
  19. package/scripts/lib/mcp-manifest.mjs +2 -2
  20. package/scripts/lib/session-presence.mjs +249 -0
  21. package/hooks/agent-route-guard.mjs +0 -113
  22. package/hooks/cross-review-tracker.mjs +0 -135
  23. package/hooks/cto-north-star-brief.mjs +0 -133
  24. package/hooks/error-context.mjs +0 -185
  25. package/hooks/hook-adaptive-collector.mjs +0 -88
  26. package/hooks/hook-manager.mjs +0 -426
  27. package/hooks/hook-registry.json +0 -367
  28. package/hooks/hooks.json +0 -149
  29. package/hooks/keyword-rules.json +0 -676
  30. package/hooks/mcp-config-watcher.mjs +0 -84
  31. package/hooks/permission-safe-allow.mjs +0 -267
  32. package/hooks/pipeline-stop.mjs +0 -435
  33. package/hooks/post-tool-tips.mjs +0 -306
  34. package/hooks/pre-compact-snapshot.mjs +0 -181
  35. package/hooks/safety-guard.mjs +0 -466
  36. package/hooks/session-end-cleanup.mjs +0 -360
  37. package/hooks/session-start-fast.mjs +0 -579
  38. package/hooks/session-start-lake.mjs +0 -81
  39. package/hooks/subagent-tracker.mjs +0 -281
  40. package/hooks/subagent-verifier.mjs +0 -85
  41. package/scripts/lib/cross-review-utils.mjs +0 -59
  42. package/scripts/lib/hook-utils.mjs +0 -16
  43. package/scripts/lib/keyword-rules.mjs +0 -296
  44. package/scripts/lib/mcp-gateway-health-check.mjs +0 -224
  45. package/scripts/lib/mcp-gateway-servers.mjs +0 -70
  46. package/scripts/lib/mcp-gateway-wrapper-check.mjs +0 -35
@@ -0,0 +1,240 @@
1
+ import { createReadStream } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { StringDecoder } from "node:string_decoder";
5
+ import { modelContext } from "./session-context.mjs";
6
+
7
+ const transcriptCache = new Map();
8
+
9
+ export async function findClaudeTranscript({
10
+ configDir,
11
+ sourceConfigDir,
12
+ sessionId,
13
+ cwd,
14
+ }) {
15
+ if (!configDir || !sessionId || /[/\\]/u.test(sessionId)) return null;
16
+ for (const dir of new Set([configDir, sourceConfigDir].filter(Boolean))) {
17
+ const projectsDir = path.join(dir, "projects");
18
+ const preferred = cwd
19
+ ? path.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-")
20
+ : null;
21
+ const projects = [
22
+ ...(preferred ? [preferred] : []),
23
+ ...(await fs.readdir(projectsDir).catch(() => [])).filter(
24
+ (project) => project !== preferred,
25
+ ),
26
+ ];
27
+ for (const project of projects) {
28
+ const candidate = path.join(projectsDir, project, `${sessionId}.jsonl`);
29
+ try {
30
+ await fs.access(candidate);
31
+ return candidate;
32
+ } catch {
33
+ // Continue looking for this session.
34
+ }
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function messageText(content) {
41
+ if (typeof content === "string") return content;
42
+ if (!Array.isArray(content)) return "";
43
+ return content
44
+ .filter((part) => part?.type === "text" && typeof part.text === "string")
45
+ .map((part) => part.text)
46
+ .join("\n");
47
+ }
48
+
49
+ function beginTurn(state) {
50
+ state.userSeen = true;
51
+ state.queued = false;
52
+ state.response = "";
53
+ state.error = null;
54
+ state.turnEnded = false;
55
+ state.sectionClosed = false;
56
+ }
57
+
58
+ function acceptEntry(state, entry, marker) {
59
+ if (entry?.isSidechain) return;
60
+ const message = entry?.message;
61
+ if (entry?.type === "system" && entry.subtype === "compact_boundary") {
62
+ state.compactCount += 1;
63
+ state.compact = {
64
+ preTokens: entry.compactMetadata?.preTokens ?? null,
65
+ postTokens: entry.compactMetadata?.postTokens ?? null,
66
+ };
67
+ state.estimatedContextTokens = state.compact.postTokens;
68
+ state.measuredAt = entry.timestamp || null;
69
+ }
70
+ if (entry?.type === "assistant") {
71
+ if (entry.isApiErrorMessage === true || message?.model === "<synthetic>") {
72
+ if (state.userSeen && !state.sectionClosed && entry.isApiErrorMessage) {
73
+ state.error = messageText(message?.content) || "Claude API error";
74
+ state.turnEnded = true;
75
+ }
76
+ return;
77
+ }
78
+ const usage = message?.usage;
79
+ if (usage) {
80
+ state.estimatedContextTokens =
81
+ (usage.input_tokens || 0) +
82
+ (usage.cache_read_input_tokens || 0) +
83
+ (usage.cache_creation_input_tokens || 0);
84
+ state.model = message.model || state.model;
85
+ state.measuredAt = entry.timestamp || state.measuredAt;
86
+ }
87
+ if (state.userSeen && !state.sectionClosed) {
88
+ const text = messageText(message?.content);
89
+ if (text) state.response = text;
90
+ if (message?.stop_reason === "end_turn") state.turnEnded = true;
91
+ }
92
+ return;
93
+ }
94
+ if (entry?.type === "user") {
95
+ const content = message?.content;
96
+ // Tool results are not a new human turn.
97
+ const humanText =
98
+ typeof content === "string" ||
99
+ (Array.isArray(content) && content.some((part) => part?.type === "text"))
100
+ ? messageText(content)
101
+ : "";
102
+ const marked =
103
+ marker &&
104
+ (humanText.includes(marker) ||
105
+ JSON.stringify(
106
+ entry.attachments ?? message?.attachments ?? [],
107
+ ).includes(marker));
108
+ if (humanText || marked) {
109
+ if (!marker || marked) beginTurn(state);
110
+ else if (state.userSeen) state.sectionClosed = true;
111
+ }
112
+ return;
113
+ }
114
+ if (
115
+ entry?.type === "attachment" &&
116
+ entry.attachment?.type === "queued_command" &&
117
+ marker &&
118
+ JSON.stringify(entry.attachment).includes(marker)
119
+ ) {
120
+ beginTurn(state);
121
+ return;
122
+ }
123
+ if (entry?.type === "queue-operation") {
124
+ const marked =
125
+ marker &&
126
+ typeof entry.content === "string" &&
127
+ entry.content.includes(marker);
128
+ if (marked && entry.operation === "enqueue") {
129
+ state.queued = true;
130
+ state.queuedContent = entry.content;
131
+ } else if (
132
+ state.queued &&
133
+ (marked || entry.content === state.queuedContent)
134
+ ) {
135
+ if (entry.operation === "remove") beginTurn(state);
136
+ } else if (marked && entry.operation === "remove") {
137
+ beginTurn(state);
138
+ }
139
+ return;
140
+ }
141
+ if (
142
+ state.userSeen &&
143
+ !state.sectionClosed &&
144
+ entry?.type === "system" &&
145
+ entry.subtype === "turn_duration"
146
+ ) {
147
+ state.turnEnded = true;
148
+ }
149
+ }
150
+
151
+ function acceptLine(state, line, marker) {
152
+ try {
153
+ acceptEntry(state, JSON.parse(line), marker);
154
+ return true;
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ export async function readClaudeTranscript(transcriptPath, { requestId } = {}) {
161
+ if (!transcriptPath) return null;
162
+ const marker = requestId ? `[tfx-live req=${requestId}]` : null;
163
+ const key = `${transcriptPath}\0${requestId || ""}`;
164
+ let stat;
165
+ try {
166
+ stat = await fs.stat(transcriptPath);
167
+ } catch (error) {
168
+ if (error?.code === "ENOENT") return null;
169
+ throw error;
170
+ }
171
+ let state = transcriptCache.get(key);
172
+ if (
173
+ !state ||
174
+ state.ino !== stat.ino ||
175
+ stat.size < state.offset ||
176
+ (stat.size === state.offset && stat.mtimeMs !== state.mtimeMs)
177
+ ) {
178
+ state = {
179
+ ino: stat.ino,
180
+ offset: 0,
181
+ pending: "",
182
+ decoder: new StringDecoder("utf8"),
183
+ userSeen: false,
184
+ queued: false,
185
+ queuedContent: null,
186
+ response: "",
187
+ error: null,
188
+ turnEnded: false,
189
+ sectionClosed: false,
190
+ estimatedContextTokens: null,
191
+ model: null,
192
+ measuredAt: null,
193
+ compactCount: 0,
194
+ compact: null,
195
+ };
196
+ }
197
+ try {
198
+ if (stat.size > state.offset) {
199
+ const stream = createReadStream(transcriptPath, {
200
+ start: state.offset,
201
+ end: stat.size - 1,
202
+ });
203
+ for await (const chunk of stream) {
204
+ const lines = (state.pending + state.decoder.write(chunk)).split("\n");
205
+ state.pending = lines.pop();
206
+ for (const line of lines) acceptLine(state, line, marker);
207
+ }
208
+ if (state.pending && acceptLine(state, state.pending, marker))
209
+ state.pending = "";
210
+ }
211
+ } catch (error) {
212
+ transcriptCache.delete(key);
213
+ if (error?.code === "ENOENT") return null;
214
+ throw error;
215
+ }
216
+ state.offset = stat.size;
217
+ state.mtimeMs = stat.mtimeMs;
218
+ transcriptCache.set(key, state);
219
+ return {
220
+ context:
221
+ state.estimatedContextTokens === null && state.model === null
222
+ ? null
223
+ : {
224
+ ...modelContext(
225
+ "claude",
226
+ state.model,
227
+ state.estimatedContextTokens,
228
+ ),
229
+ model: state.model,
230
+ measuredAt: state.measuredAt,
231
+ },
232
+ userSeen: state.userSeen,
233
+ response: state.response,
234
+ error: state.error,
235
+ turnEnded: state.turnEnded,
236
+ sectionClosed: state.sectionClosed,
237
+ compactCount: state.compactCount,
238
+ compact: state.compact,
239
+ };
240
+ }
@@ -0,0 +1,203 @@
1
+ import { createReadStream } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ import { StringDecoder } from "node:string_decoder";
6
+
7
+ // https://platform.claude.com/docs/en/build-with-claude/context-windows 2026-10-08
8
+ // https://developers.openai.com/api/docs/models 2026-10-08
9
+ const MODEL_CONTEXT = [
10
+ {
11
+ cli: "claude",
12
+ model: /^(?:claude-)?(?:fable-5[.-]1|opus-5[.-]5|sonnet-5[.-]5)(?:$|[-[])/i,
13
+ tokens: 1_000_000,
14
+ source:
15
+ "https://platform.claude.com/docs/en/build-with-claude/context-windows",
16
+ },
17
+ {
18
+ cli: "codex",
19
+ model: /^gpt-6(?:\.\d+)?(?:$|[-_])/i,
20
+ tokens: 1_050_000,
21
+ source: "https://developers.openai.com/api/docs/models",
22
+ },
23
+ ];
24
+
25
+ export const CONTEXT_THRESHOLDS = {
26
+ claude: { warnContextPct: 60, maxContextPct: 90 },
27
+ codex: { warnContextPct: 15, maxContextPct: 22 },
28
+ };
29
+
30
+ const rolloutCache = new Map();
31
+
32
+ export function modelContext(cli, model, estimatedContextTokens = null) {
33
+ const entry = MODEL_CONTEXT.find(
34
+ (item) => item.cli === cli && item.model.test(model || ""),
35
+ );
36
+ const contextLimitTokens = entry?.tokens ?? null;
37
+ const executionContextLimitTokens =
38
+ cli === "claude" && process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT === "1"
39
+ ? 200_000
40
+ : null;
41
+ const effectiveLimit = Math.min(
42
+ contextLimitTokens ?? Infinity,
43
+ executionContextLimitTokens ?? Infinity,
44
+ );
45
+ return {
46
+ estimatedContextTokens,
47
+ contextLimitTokens,
48
+ contextLimitSource: entry?.source ?? null,
49
+ ...(executionContextLimitTokens ? { executionContextLimitTokens } : {}),
50
+ contextPct:
51
+ contextLimitTokens && Number.isFinite(estimatedContextTokens)
52
+ ? (estimatedContextTokens / effectiveLimit) * 100
53
+ : null,
54
+ };
55
+ }
56
+
57
+ export function contextGuard(
58
+ cli,
59
+ context,
60
+ { warnContextPct, maxContextPct } = {},
61
+ ) {
62
+ const defaults = CONTEXT_THRESHOLDS[cli];
63
+ const warn = warnContextPct ?? defaults?.warnContextPct ?? 0;
64
+ const max = maxContextPct ?? defaults?.maxContextPct ?? 0;
65
+ const fields = {
66
+ estimatedContextTokens: context?.estimatedContextTokens ?? null,
67
+ contextLimitTokens: context?.contextLimitTokens ?? null,
68
+ contextLimitSource: context?.contextLimitSource ?? null,
69
+ executionContextLimitTokens: context?.executionContextLimitTokens ?? null,
70
+ contextPct: context?.contextPct ?? null,
71
+ };
72
+ if (fields.contextPct === null) {
73
+ return warn > 0
74
+ ? {
75
+ contextWarning: {
76
+ ...fields,
77
+ reason:
78
+ fields.contextLimitTokens === null
79
+ ? "unknown-model"
80
+ : "usage-unavailable",
81
+ },
82
+ }
83
+ : {};
84
+ }
85
+ if (max > 0 && fields.contextPct >= max) {
86
+ return {
87
+ ok: false,
88
+ errorCode: "context-limit",
89
+ inputSent: false,
90
+ hint: "세션을 승계한 뒤 새 세션에 요청하라.",
91
+ ...fields,
92
+ };
93
+ }
94
+ return warn > 0 && fields.contextPct >= warn
95
+ ? { contextWarning: { ...fields, thresholdPct: warn } }
96
+ : {};
97
+ }
98
+
99
+ async function findCodexRollout(threadId) {
100
+ if (!/^[a-zA-Z0-9-]+$/u.test(threadId || "")) return null;
101
+ const root = path.join(
102
+ process.env.CODEX_HOME || path.join(homedir(), ".codex"),
103
+ "sessions",
104
+ );
105
+ for (const year of (await fs.readdir(root).catch(() => []))
106
+ .sort()
107
+ .reverse()) {
108
+ if (!/^\d{4}$/u.test(year)) continue;
109
+ for (const month of (
110
+ await fs.readdir(path.join(root, year)).catch(() => [])
111
+ )
112
+ .sort()
113
+ .reverse()) {
114
+ if (!/^\d{2}$/u.test(month)) continue;
115
+ for (const day of (
116
+ await fs.readdir(path.join(root, year, month)).catch(() => [])
117
+ )
118
+ .sort()
119
+ .reverse()) {
120
+ if (!/^\d{2}$/u.test(day)) continue;
121
+ const dir = path.join(root, year, month, day);
122
+ const file = (await fs.readdir(dir).catch(() => [])).find((name) =>
123
+ name.endsWith(`-${threadId}.jsonl`),
124
+ );
125
+ if (file) return path.join(dir, file);
126
+ }
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+
132
+ function acceptCodexLine(state, line) {
133
+ let entry;
134
+ try {
135
+ entry = JSON.parse(line);
136
+ } catch {
137
+ return false;
138
+ }
139
+ if (entry?.type === "turn_context")
140
+ state.model = entry.payload?.model || state.model;
141
+ if (entry?.type === "event_msg" && entry.payload?.type === "token_count") {
142
+ const info = entry.payload.info;
143
+ if (Number.isFinite(info?.last_token_usage?.total_tokens))
144
+ state.tokens = info.last_token_usage.total_tokens;
145
+ if (Number.isFinite(info?.model_context_window))
146
+ state.executionContextLimitTokens = info.model_context_window;
147
+ }
148
+ return true;
149
+ }
150
+
151
+ export async function readCodexContext(rolloutPath, threadId) {
152
+ rolloutPath ||= await findCodexRollout(threadId);
153
+ if (!rolloutPath) return { ...modelContext("codex", null), model: null };
154
+ let stat;
155
+ try {
156
+ stat = await fs.stat(rolloutPath);
157
+ } catch {
158
+ return { ...modelContext("codex", null), model: null };
159
+ }
160
+ let state = rolloutCache.get(rolloutPath);
161
+ if (
162
+ !state ||
163
+ state.ino !== stat.ino ||
164
+ stat.size < state.offset ||
165
+ (stat.size === state.offset && stat.mtimeMs !== state.mtimeMs)
166
+ ) {
167
+ state = {
168
+ ino: stat.ino,
169
+ offset: 0,
170
+ pending: "",
171
+ decoder: new StringDecoder("utf8"),
172
+ model: null,
173
+ tokens: null,
174
+ executionContextLimitTokens: null,
175
+ };
176
+ }
177
+ try {
178
+ if (stat.size > state.offset) {
179
+ const stream = createReadStream(rolloutPath, {
180
+ start: state.offset,
181
+ end: stat.size - 1,
182
+ });
183
+ for await (const chunk of stream) {
184
+ const lines = (state.pending + state.decoder.write(chunk)).split("\n");
185
+ state.pending = lines.pop();
186
+ for (const line of lines) acceptCodexLine(state, line);
187
+ }
188
+ if (state.pending && acceptCodexLine(state, state.pending))
189
+ state.pending = "";
190
+ }
191
+ } catch {
192
+ rolloutCache.delete(rolloutPath);
193
+ return { ...modelContext("codex", null), model: null };
194
+ }
195
+ state.offset = stat.size;
196
+ state.mtimeMs = stat.mtimeMs;
197
+ rolloutCache.set(rolloutPath, state);
198
+ return {
199
+ ...modelContext("codex", state.model, state.tokens),
200
+ model: state.model,
201
+ executionContextLimitTokens: state.executionContextLimitTokens,
202
+ };
203
+ }
package/hud/constants.mjs CHANGED
@@ -28,6 +28,16 @@ export const ACCOUNTS_STATE_PATH = join(
28
28
  "cli_accounts_state.json",
29
29
  );
30
30
 
31
+ // triflux-mods 의 Claude band 가 세션마다 갱신하는 표식. 있으면 Claude 행을 뺀다.
32
+ export const CLAUDE_BAND_MARKER_DIR = join(
33
+ homedir(),
34
+ ".claude",
35
+ "cache",
36
+ "triflux",
37
+ "claude-band",
38
+ );
39
+ export const CLAUDE_BAND_MARKER_TTL_MS = 12 * 60 * 60 * 1000;
40
+
31
41
  // tfx-multi 상태 (v2.2 HUD 통합)
32
42
  export const TEAM_STATE_PATH = join(
33
43
  homedir(),
@@ -5,6 +5,8 @@
5
5
  // 각 모듈에서 색상, 터미널, 프로바이더, 렌더러를 가져와 조합한다.
6
6
  // ============================================================================
7
7
 
8
+ import { statSync } from "node:fs";
9
+ import { join } from "node:path";
8
10
  import {
9
11
  resolveHudCliVisibility,
10
12
  shouldRenderGeminiFallbackRow,
@@ -22,6 +24,8 @@ import {
22
24
  import {
23
25
  ACCOUNTS_CONFIG_PATH,
24
26
  ACCOUNTS_STATE_PATH,
27
+ CLAUDE_BAND_MARKER_DIR,
28
+ CLAUDE_BAND_MARKER_TTL_MS,
25
29
  CLAUDE_REFRESH_FLAG,
26
30
  CODEX_REFRESH_FLAG,
27
31
  GEMINI_FLASH_POOL,
@@ -54,8 +58,8 @@ import {
54
58
  buildGeminiAuthContext,
55
59
  deriveGeminiFamilyBucket,
56
60
  fetchGeminiQuota,
61
+ getAntigravityAccountLabel,
57
62
  getAntigravityCurrentModel,
58
- getAntigravityEmail,
59
63
  getAntigravityModelAbbrev,
60
64
  getAntigravityModelFamily,
61
65
  getGeminiEmail,
@@ -167,7 +171,7 @@ async function main() {
167
171
  : claudeUsageSnapshot.data;
168
172
  const codexEmail = getCodexEmail();
169
173
  const geminiEmail = getGeminiEmail();
170
- const antigravityEmail = getAntigravityEmail();
174
+ const antigravityAccountLabel = getAntigravityAccountLabel();
171
175
  const codexBuckets = codexSnapshot.buckets;
172
176
  const geminiSession = geminiSessionSnapshot.session;
173
177
  const geminiQuota = geminiQuotaSnapshot.quota;
@@ -237,7 +241,11 @@ async function main() {
237
241
  session: geminiSession,
238
242
  };
239
243
 
240
- const rows = [...getClaudeRows(CURRENT_TIER, contextView, claudeUsage)];
244
+ const rows = [
245
+ ...(isClaudeBandActive(stdin?.session_id)
246
+ ? []
247
+ : getClaudeRows(CURRENT_TIER, contextView, claudeUsage)),
248
+ ];
241
249
 
242
250
  // 정책으로 생략된 행이 있으면 뒤따르는 행의 인덱스가 밀린다. dim 래핑이
243
251
  // 하드코딩 인덱스를 쓰지 않도록 실제 위치를 기록해 둔다.
@@ -284,7 +292,7 @@ async function main() {
284
292
  currentFamily: antigravityModelFamily,
285
293
  }
286
294
  : geminiQuotaData,
287
- antigravityReady ? antigravityEmail : geminiEmail,
295
+ antigravityReady ? antigravityAccountLabel : geminiEmail,
288
296
  null,
289
297
  ),
290
298
  );
@@ -357,3 +365,14 @@ main().catch(() => {
357
365
  `\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`,
358
366
  );
359
367
  });
368
+
369
+ // 프롬프트 위 band 가 같은 정보를 그리는 세션에서는 Claude 행을 중복해 그리지 않는다.
370
+ function isClaudeBandActive(sessionId) {
371
+ if (!sessionId) return false;
372
+ try {
373
+ const { mtimeMs } = statSync(join(CLAUDE_BAND_MARKER_DIR, sessionId));
374
+ return Date.now() - mtimeMs < CLAUDE_BAND_MARKER_TTL_MS;
375
+ } catch {
376
+ return false;
377
+ }
378
+ }
@@ -136,7 +136,10 @@ export function getGeminiEmail() {
136
136
  }
137
137
  }
138
138
 
139
- export function getAntigravityEmail() {
139
+ // GCP 프로젝트 인증이면 계정 이메일 대신 프로젝트 ID 를 보여 준다.
140
+ export function getAntigravityAccountLabel() {
141
+ const project = readJson(ANTIGRAVITY_SETTINGS_PATH, null)?.gcp?.project;
142
+ if (project) return project;
140
143
  for (const oauthPath of ANTIGRAVITY_OAUTH_PATHS) {
141
144
  try {
142
145
  const oauth = readJson(oauthPath, null);
package/hud/renderers.mjs CHANGED
@@ -314,12 +314,16 @@ export function getMicroLine(
314
314
  if (showGemini) {
315
315
  segments.push(`${bold(geminiBlue(geminiMarker))}${dim(":")}${gVal}`);
316
316
  }
317
- segments.push(
318
- `${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}`,
319
- );
317
+ segments.push(`${dim("CTX:")}${contextPercentText(ctxView)}`);
320
318
  return truncateAnsi(segments.join(" "), cols);
321
319
  }
322
320
 
321
+ // context 는 토큰 수 대신 사용률만 보여 준다.
322
+ function contextPercentText(ctxView) {
323
+ const text = ctxView.display === "--" ? "--" : `${ctxView.percent}%`;
324
+ return colorByPercent(ctxView.percent, text);
325
+ }
326
+
323
327
  // ============================================================================
324
328
  // Claude 행 렌더러
325
329
  // ============================================================================
@@ -381,7 +385,7 @@ export function getClaudeRows(currentTier, contextView, claudeUsage) {
381
385
  if (currentTier === "minimal") {
382
386
  const staleTag = claudeUsage?.stale ? ` ${dim("[stale]")}` : "";
383
387
  const quotaSection = `${dim("5h:")}${fStr} ${dim("1w:")}${wStr}${staleTag}`;
384
- const right = `${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}`;
388
+ const right = `${dim("CTX:")}${contextPercentText(ctxView)}`;
385
389
  return [{ prefix, left: quotaSection, right }];
386
390
  }
387
391
 
@@ -391,7 +395,7 @@ export function getClaudeRows(currentTier, contextView, claudeUsage) {
391
395
  const warning = ctxView.warningTag
392
396
  ? ` ${dim("|")} ${yellow(ctxView.warningTag)}`
393
397
  : "";
394
- const contextSection = `${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}${warning}`;
398
+ const contextSection = `${dim("CTX:")}${contextPercentText(ctxView)}${warning}`;
395
399
  return [{ prefix, left: quotaSection, right: contextSection }];
396
400
  }
397
401
 
@@ -401,7 +405,7 @@ export function getClaudeRows(currentTier, contextView, claudeUsage) {
401
405
  const warning = ctxView.warningTag
402
406
  ? ` ${dim("|")} ${yellow(ctxView.warningTag)}`
403
407
  : "";
404
- const contextSection = `${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}${warning}`;
408
+ const contextSection = `${dim("CTX:")}${contextPercentText(ctxView)}${warning}`;
405
409
  return [{ prefix, left: quotaSection, right: contextSection }];
406
410
  }
407
411
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/core",
3
- "version": "10.49.0",
3
+ "version": "10.50.0",
4
4
  "description": "triflux core — CLI routing, pipeline, adapters. Zero native dependencies.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -34,88 +34,3 @@ export function inspectMacTimeoutDependency({
34
34
  fix: "brew install coreutils",
35
35
  };
36
36
  }
37
-
38
- export function addPluginRootFallbackToCommand(command, pluginRoot) {
39
- if (typeof command !== "string") return command;
40
- const fallbackRoot = String(pluginRoot || "").replace(/\\/g, "/");
41
- if (!fallbackRoot) return command;
42
- return command
43
- .replaceAll("${PLUGIN_ROOT}", `\${PLUGIN_ROOT:-${fallbackRoot}}`)
44
- .replaceAll(
45
- "${CLAUDE_PLUGIN_ROOT}",
46
- `\${CLAUDE_PLUGIN_ROOT:-${fallbackRoot}}`,
47
- )
48
- .replace(
49
- /(^|[\s"'])\/(hooks|scripts)\//g,
50
- `$1\${PLUGIN_ROOT:-${fallbackRoot}}/$2/`,
51
- );
52
- }
53
-
54
- function hasBarePluginRootReference(command) {
55
- if (typeof command !== "string") return false;
56
- return (
57
- command.includes("${PLUGIN_ROOT}/") ||
58
- command.includes("${CLAUDE_PLUGIN_ROOT}/")
59
- );
60
- }
61
-
62
- function hasCollapsedRootHookPath(command) {
63
- if (typeof command !== "string") return false;
64
- return /(^|[\s"'])\/(?:hooks|scripts)\//.test(command);
65
- }
66
-
67
- export function findPluginRootHookIssues(settings) {
68
- const hooksByEvent =
69
- settings?.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
70
- const issues = [];
71
-
72
- for (const [event, entries] of Object.entries(hooksByEvent)) {
73
- if (!Array.isArray(entries)) continue;
74
- for (const entry of entries) {
75
- const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
76
- for (const hook of hooks) {
77
- const command = hook?.command;
78
- if (
79
- !hasBarePluginRootReference(command) &&
80
- !hasCollapsedRootHookPath(command)
81
- ) {
82
- continue;
83
- }
84
- issues.push({
85
- event,
86
- matcher: entry?.matcher || "*",
87
- command,
88
- reason: hasCollapsedRootHookPath(command)
89
- ? "collapsed-root-path"
90
- : "missing-plugin-root-fallback",
91
- });
92
- }
93
- }
94
- }
95
-
96
- return issues;
97
- }
98
-
99
- export function applyPluginRootHookFallbacks(settings, { pluginRoot } = {}) {
100
- if (!settings?.hooks || typeof settings.hooks !== "object") {
101
- return { changed: false, count: 0, settings };
102
- }
103
-
104
- let count = 0;
105
- for (const entries of Object.values(settings.hooks)) {
106
- if (!Array.isArray(entries)) continue;
107
- for (const entry of entries) {
108
- const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
109
- for (const hook of hooks) {
110
- if (typeof hook?.command !== "string") continue;
111
- const next = addPluginRootFallbackToCommand(hook.command, pluginRoot);
112
- if (next !== hook.command) {
113
- hook.command = next;
114
- count++;
115
- }
116
- }
117
- }
118
- }
119
-
120
- return { changed: count > 0, count, settings };
121
- }