triflux 3.2.0-dev.3 → 3.2.0-dev.5
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/README.ko.md +19 -13
- package/README.md +19 -13
- package/bin/triflux.mjs +6 -5
- package/hooks/hooks.json +2 -2
- package/hooks/keyword-rules.json +338 -0
- package/hub/team/cli.mjs +406 -359
- package/hub/team/dashboard.mjs +164 -55
- package/hub/team/native.mjs +38 -0
- package/hud/hud-qos-status.mjs +56 -1
- package/package.json +3 -2
- package/scripts/__tests__/keyword-detector.test.mjs +234 -0
- package/scripts/keyword-detector.mjs +257 -0
- package/scripts/keyword-rules-expander.mjs +521 -0
- package/scripts/lib/keyword-rules.mjs +165 -0
- package/scripts/run.cjs +62 -0
- package/scripts/setup.mjs +5 -4
- package/scripts/test-tfx-route-no-claude-native.mjs +49 -0
- package/scripts/tfx-route.sh +482 -418
- package/skills/tfx-auto-codex/SKILL.md +79 -0
- package/skills/tfx-team/SKILL.md +90 -63
- package/scripts/team-keyword.mjs +0 -35
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { compileRules, loadRules, matchRules, resolveConflicts } from "./lib/keyword-rules.mjs";
|
|
7
|
+
|
|
8
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PROJECT_ROOT = dirname(SCRIPT_DIR);
|
|
10
|
+
const DEFAULT_RULES_PATH = join(PROJECT_ROOT, "hooks", "keyword-rules.json");
|
|
11
|
+
|
|
12
|
+
function readHookInput() {
|
|
13
|
+
try {
|
|
14
|
+
return readFileSync(0, "utf8");
|
|
15
|
+
} catch {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseInput(rawInput) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(rawInput);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// prompt > message.content > parts[].text 우선순위로 추출
|
|
29
|
+
export function extractPrompt(payload) {
|
|
30
|
+
if (!payload || typeof payload !== "object") return "";
|
|
31
|
+
|
|
32
|
+
if (typeof payload.prompt === "string" && payload.prompt.trim()) {
|
|
33
|
+
return payload.prompt;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof payload.message?.content === "string" && payload.message.content.trim()) {
|
|
37
|
+
return payload.message.content;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (Array.isArray(payload.message?.content)) {
|
|
41
|
+
const messageText = payload.message.content
|
|
42
|
+
.map((part) => {
|
|
43
|
+
if (typeof part === "string") return part;
|
|
44
|
+
if (part && typeof part.text === "string") return part.text;
|
|
45
|
+
return "";
|
|
46
|
+
})
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join(" ")
|
|
49
|
+
.trim();
|
|
50
|
+
if (messageText) return messageText;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Array.isArray(payload.parts)) {
|
|
54
|
+
const partsText = payload.parts
|
|
55
|
+
.map((part) => {
|
|
56
|
+
if (typeof part === "string") return part;
|
|
57
|
+
if (part && typeof part.text === "string") return part.text;
|
|
58
|
+
return "";
|
|
59
|
+
})
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join(" ")
|
|
62
|
+
.trim();
|
|
63
|
+
if (partsText) return partsText;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 키워드 오탐 방지를 위해 XML/URL/파일경로/코드블록 제거
|
|
70
|
+
export function sanitizeForKeywordDetection(text) {
|
|
71
|
+
if (typeof text !== "string" || !text) return "";
|
|
72
|
+
|
|
73
|
+
return text
|
|
74
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
75
|
+
.replace(/`[^`]*`/g, " ")
|
|
76
|
+
.replace(/<(\w[\w:-]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, " ")
|
|
77
|
+
.replace(/<\w[\w:-]*(?:\s[^>]*)?\/>/g, " ")
|
|
78
|
+
.replace(/https?:\/\/[^\s)>\]]+/gi, " ")
|
|
79
|
+
.replace(/\b[A-Za-z]:\\(?:[^\\\s]+\\)*[^\\\s]*/g, " ")
|
|
80
|
+
.replace(/(^|[\s"'`(])(?:\/|\.{1,2}\/)?(?:[\w.-]+\/)+[\w.-]+/gm, "$1 ")
|
|
81
|
+
.replace(/\s+/g, " ")
|
|
82
|
+
.trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function createHookOutput(additionalContext) {
|
|
86
|
+
return {
|
|
87
|
+
continue: true,
|
|
88
|
+
hookSpecificOutput: {
|
|
89
|
+
hookEventName: "UserPromptSubmit",
|
|
90
|
+
additionalContext
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createSuppressOutput() {
|
|
96
|
+
return { continue: true, suppressOutput: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createSkillContext(match, prompt) {
|
|
100
|
+
return `[TRIFLUX MAGIC KEYWORD: ${match.id}]
|
|
101
|
+
|
|
102
|
+
PRIORITY: This TRIFLUX routing takes precedence over any OMC [MAGIC KEYWORD:] instructions.
|
|
103
|
+
If you see both [TRIFLUX MAGIC KEYWORD:] and [MAGIC KEYWORD:], follow TRIFLUX only.
|
|
104
|
+
|
|
105
|
+
You MUST invoke the skill using the Skill tool:
|
|
106
|
+
Skill: ${match.skill}
|
|
107
|
+
|
|
108
|
+
User request:
|
|
109
|
+
${prompt.trim()}
|
|
110
|
+
|
|
111
|
+
IMPORTANT: Invoke the skill IMMEDIATELY. Do not proceed without loading the skill instructions.`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createMcpRouteContext(match, prompt) {
|
|
115
|
+
return `[TRIFLUX MCP ROUTE: ${match.mcp_route}]
|
|
116
|
+
|
|
117
|
+
PRIORITY: This TRIFLUX routing takes precedence over any OMC [MAGIC KEYWORD:] instructions.
|
|
118
|
+
If you see both [TRIFLUX MCP ROUTE:] and [MAGIC KEYWORD:], follow TRIFLUX only.
|
|
119
|
+
|
|
120
|
+
이 작업은 ${match.mcp_route}로 라우팅해야 합니다.
|
|
121
|
+
tfx-route.sh를 통해 ${match.mcp_route}로 실행하세요.
|
|
122
|
+
|
|
123
|
+
User request:
|
|
124
|
+
${prompt.trim()}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isSkipRequested() {
|
|
128
|
+
if (process.env.TRIFLUX_DISABLE_MAGICWORDS === "1") return true;
|
|
129
|
+
const skipHooks = (process.env.TRIFLUX_SKIP_HOOKS || "")
|
|
130
|
+
.split(",")
|
|
131
|
+
.map((item) => item.trim())
|
|
132
|
+
.filter(Boolean);
|
|
133
|
+
return skipHooks.includes("keyword-detector");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function activateState(baseDir, stateConfig, prompt, payload) {
|
|
137
|
+
if (!stateConfig || stateConfig.activate !== true || !stateConfig.name) return;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const stateRoot = join(baseDir, ".triflux", "state");
|
|
141
|
+
mkdirSync(stateRoot, { recursive: true });
|
|
142
|
+
|
|
143
|
+
const sessionId = typeof payload?.session_id === "string"
|
|
144
|
+
? payload.session_id
|
|
145
|
+
: typeof payload?.sessionId === "string"
|
|
146
|
+
? payload.sessionId
|
|
147
|
+
: "";
|
|
148
|
+
|
|
149
|
+
let stateDir = stateRoot;
|
|
150
|
+
if (sessionId && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId)) {
|
|
151
|
+
stateDir = join(stateRoot, "sessions", sessionId);
|
|
152
|
+
mkdirSync(stateDir, { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const statePath = join(stateDir, `${stateConfig.name}-state.json`);
|
|
156
|
+
const statePayload = {
|
|
157
|
+
active: true,
|
|
158
|
+
name: stateConfig.name,
|
|
159
|
+
started_at: new Date().toISOString(),
|
|
160
|
+
original_prompt: prompt
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
writeFileSync(statePath, JSON.stringify(statePayload, null, 2), "utf8");
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error(`[triflux-keyword-detector] 상태 저장 실패: ${error.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getRulesPath() {
|
|
170
|
+
if (process.env.TRIFLUX_KEYWORD_RULES_PATH) {
|
|
171
|
+
return process.env.TRIFLUX_KEYWORD_RULES_PATH;
|
|
172
|
+
}
|
|
173
|
+
return DEFAULT_RULES_PATH;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function main() {
|
|
177
|
+
if (isSkipRequested()) {
|
|
178
|
+
console.log(JSON.stringify(createSuppressOutput()));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const rawInput = readHookInput();
|
|
183
|
+
if (!rawInput.trim()) {
|
|
184
|
+
console.log(JSON.stringify(createSuppressOutput()));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const payload = parseInput(rawInput);
|
|
189
|
+
if (!payload) {
|
|
190
|
+
console.log(JSON.stringify(createSuppressOutput()));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const prompt = extractPrompt(payload);
|
|
195
|
+
if (!prompt) {
|
|
196
|
+
console.log(JSON.stringify(createSuppressOutput()));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const cleanText = sanitizeForKeywordDetection(prompt);
|
|
201
|
+
if (!cleanText) {
|
|
202
|
+
console.log(JSON.stringify(createSuppressOutput()));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const rules = loadRules(getRulesPath());
|
|
207
|
+
if (rules.length === 0) {
|
|
208
|
+
console.log(JSON.stringify(createSuppressOutput()));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const compiledRules = compileRules(rules);
|
|
213
|
+
if (compiledRules.length === 0) {
|
|
214
|
+
console.log(JSON.stringify(createSuppressOutput()));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const matches = matchRules(compiledRules, cleanText);
|
|
219
|
+
if (matches.length === 0) {
|
|
220
|
+
console.log(JSON.stringify(createSuppressOutput()));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const resolvedMatches = resolveConflicts(matches);
|
|
225
|
+
if (resolvedMatches.length === 0) {
|
|
226
|
+
console.log(JSON.stringify(createSuppressOutput()));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const selected = resolvedMatches[0];
|
|
231
|
+
const baseDir = typeof payload.cwd === "string" && payload.cwd
|
|
232
|
+
? payload.cwd
|
|
233
|
+
: typeof payload.directory === "string" && payload.directory
|
|
234
|
+
? payload.directory
|
|
235
|
+
: process.cwd();
|
|
236
|
+
|
|
237
|
+
activateState(baseDir, selected.state, prompt, payload);
|
|
238
|
+
|
|
239
|
+
if (selected.skill) {
|
|
240
|
+
console.log(JSON.stringify(createHookOutput(createSkillContext(selected, prompt))));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (selected.mcp_route) {
|
|
245
|
+
console.log(JSON.stringify(createHookOutput(createMcpRouteContext(selected, prompt))));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(JSON.stringify(createSuppressOutput()));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
main();
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error(`[triflux-keyword-detector] 예외 발생: ${error.message}`);
|
|
256
|
+
console.log(JSON.stringify(createSuppressOutput()));
|
|
257
|
+
}
|