@triflux/core 10.49.0 → 10.50.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/agy-session-hook.mjs +1 -1
- package/hooks/claude-cwd-projection-refresh.mjs +7 -79
- package/hooks/codex-session-hook.mjs +3 -8
- package/hooks/hook-orchestrator.mjs +8 -648
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hub/bridge.mjs +290 -51
- package/hub/team/claude-daemon-control.mjs +54 -6
- package/hub/team/claude-transcript.mjs +240 -0
- package/hub/team/session-context.mjs +203 -0
- package/hud/constants.mjs +10 -0
- package/hud/hud-qos-status.mjs +23 -4
- package/hud/providers/gemini.mjs +4 -1
- package/hud/renderers.mjs +10 -6
- package/package.json +1 -1
- package/scripts/lib/doctor-env-checks.mjs +0 -85
- package/scripts/lib/legacy-hook-cleanup.mjs +230 -0
- package/scripts/lib/legacy-mcp-cleanup.mjs +772 -0
- package/scripts/lib/mcp-guard-engine.mjs +44 -271
- package/scripts/lib/mcp-manifest.mjs +2 -2
- package/scripts/lib/session-presence.mjs +249 -0
- package/hooks/agent-route-guard.mjs +0 -113
- package/hooks/cross-review-tracker.mjs +0 -135
- package/hooks/cto-north-star-brief.mjs +0 -133
- package/hooks/error-context.mjs +0 -185
- package/hooks/hook-adaptive-collector.mjs +0 -88
- package/hooks/hook-manager.mjs +0 -426
- package/hooks/hook-registry.json +0 -367
- package/hooks/hooks.json +0 -149
- package/hooks/keyword-rules.json +0 -676
- package/hooks/mcp-config-watcher.mjs +0 -84
- package/hooks/permission-safe-allow.mjs +0 -267
- package/hooks/pipeline-stop.mjs +0 -435
- package/hooks/post-tool-tips.mjs +0 -306
- package/hooks/pre-compact-snapshot.mjs +0 -181
- package/hooks/safety-guard.mjs +0 -466
- package/hooks/session-end-cleanup.mjs +0 -360
- package/hooks/session-start-fast.mjs +0 -579
- package/hooks/session-start-lake.mjs +0 -81
- package/hooks/subagent-tracker.mjs +0 -281
- package/hooks/subagent-verifier.mjs +0 -85
- package/scripts/lib/cross-review-utils.mjs +0 -59
- package/scripts/lib/hook-utils.mjs +0 -16
- package/scripts/lib/keyword-rules.mjs +0 -296
- package/scripts/lib/mcp-gateway-health-check.mjs +0 -224
- package/scripts/lib/mcp-gateway-servers.mjs +0 -70
- 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(),
|
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -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
|
|
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 = [
|
|
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 ?
|
|
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
|
+
}
|
package/hud/providers/gemini.mjs
CHANGED
|
@@ -136,7 +136,10 @@ export function getGeminiEmail() {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
|
|
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:")}${
|
|
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:")}${
|
|
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:")}${
|
|
408
|
+
const contextSection = `${dim("CTX:")}${contextPercentText(ctxView)}${warning}`;
|
|
405
409
|
return [{ prefix, left: quotaSection, right: contextSection }];
|
|
406
410
|
}
|
|
407
411
|
|
package/package.json
CHANGED
|
@@ -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
|
-
}
|