triflux 10.3.4 → 10.4.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 (120) hide show
  1. package/CLAUDE.md +193 -0
  2. package/LICENSE +21 -21
  3. package/hooks/hook-registry.json +256 -256
  4. package/hub/adaptive-inject.mjs +1 -1
  5. package/hub/assign-callbacks.mjs +120 -120
  6. package/hub/delegator/index.mjs +14 -14
  7. package/hub/delegator/tool-definitions.mjs +35 -35
  8. package/hub/hitl.mjs +143 -143
  9. package/hub/router.mjs +791 -791
  10. package/hub/session-fingerprint.mjs +1 -1
  11. package/hub/team/cli/commands/attach.mjs +37 -37
  12. package/hub/team/cli/commands/debug.mjs +74 -74
  13. package/hub/team/cli/commands/focus.mjs +53 -53
  14. package/hub/team/cli/commands/list.mjs +24 -24
  15. package/hub/team/cli/commands/start/start-in-process.mjs +40 -40
  16. package/hub/team/cli/commands/start/start-mux.mjs +73 -73
  17. package/hub/team/cli/commands/start/start-wt.mjs +69 -69
  18. package/hub/team/cli/commands/tasks.mjs +13 -13
  19. package/hub/team/cli/render.mjs +30 -30
  20. package/hub/team/cli/services/attach-fallback.mjs +54 -54
  21. package/hub/team/cli/services/member-selector.mjs +30 -30
  22. package/hub/team/cli/services/native-control.mjs +116 -116
  23. package/hub/team/cli/services/task-model.mjs +30 -30
  24. package/hub/team/notify.mjs +1 -1
  25. package/hub/team/orchestrator.mjs +161 -161
  26. package/hub/team/session.mjs +611 -611
  27. package/hub/team/shared.mjs +13 -13
  28. package/hub/tray.mjs +368 -368
  29. package/hub/workers/codex-mcp.mjs +507 -507
  30. package/hub/workers/factory.mjs +21 -21
  31. package/package.json +21 -55
  32. package/references/hosts.json +33 -0
  33. package/scripts/completions/tfx.bash +47 -47
  34. package/scripts/completions/tfx.fish +44 -44
  35. package/scripts/completions/tfx.zsh +83 -83
  36. package/scripts/hub-ensure.mjs +120 -120
  37. package/scripts/keyword-detector.mjs +272 -272
  38. package/scripts/keyword-rules-expander.mjs +521 -521
  39. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  40. package/scripts/notion-read.mjs +553 -553
  41. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  42. package/scripts/tfx-batch-stats.mjs +96 -96
  43. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
  44. package/skills/.omc/state/idle-notif-cooldown.json +3 -0
  45. package/skills/.omc/state/last-tool-error.json +7 -0
  46. package/skills/.omc/state/subagent-tracking.json +7 -0
  47. package/skills/tfx-remote-spawn/references/hosts.json +16 -0
  48. package/skills/tfx-workspace/async-tests/run-tests.sh +203 -0
  49. package/skills/tfx-workspace/evals/evals.json +79 -0
  50. package/skills/tfx-workspace/iteration-1/benchmark.json +162 -0
  51. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +11 -0
  52. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +9 -0
  53. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +154 -0
  54. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +5 -0
  55. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +9 -0
  56. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +126 -0
  57. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +5 -0
  58. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +11 -0
  59. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +9 -0
  60. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +119 -0
  61. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +5 -0
  62. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +9 -0
  63. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +115 -0
  64. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +5 -0
  65. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +10 -0
  66. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +8 -0
  67. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +86 -0
  68. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +5 -0
  69. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +8 -0
  70. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +81 -0
  71. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +5 -0
  72. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +12 -0
  73. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +10 -0
  74. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +316 -0
  75. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +5 -0
  76. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +10 -0
  77. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +352 -0
  78. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +5 -0
  79. package/skills/tfx-workspace/iteration-1/review.html +1325 -0
  80. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +12 -0
  81. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +10 -0
  82. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +97 -0
  83. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +5 -0
  84. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +10 -0
  85. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +94 -0
  86. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +5 -0
  87. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +12 -0
  88. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +10 -0
  89. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +209 -0
  90. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +5 -0
  91. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +10 -0
  92. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +193 -0
  93. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +5 -0
  94. package/skills/tfx-workspace/iteration-2/benchmark.json +62 -0
  95. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +13 -0
  96. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +11 -0
  97. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +382 -0
  98. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +5 -0
  99. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +11 -0
  100. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +333 -0
  101. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +5 -0
  102. package/skills/tfx-workspace/iteration-2/review.html +1325 -0
  103. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +217 -0
  104. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +77 -0
  105. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +65 -0
  106. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +94 -0
  107. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +82 -0
  108. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +133 -0
  109. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +426 -0
  110. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +101 -0
  111. package/.claude-plugin/marketplace.json +0 -34
  112. package/.claude-plugin/plugin.json +0 -22
  113. package/config/mcp-registry.json +0 -29
  114. package/tui/codex-profile.mjs +0 -402
  115. package/tui/core.mjs +0 -236
  116. package/tui/doctor.mjs +0 -328
  117. package/tui/gemini-profile.mjs +0 -254
  118. package/tui/monitor-data.mjs +0 -148
  119. package/tui/monitor.mjs +0 -295
  120. package/tui/setup.mjs +0 -442
@@ -1,272 +1,272 @@
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 createSuppressOmcContext(match, prompt) {
115
- return `[TRIFLUX OMC SUPPRESS: ${match.id}]
116
-
117
- PRIORITY: TRIFLUX가 이 키워드를 관리합니다. OMC [MAGIC KEYWORD:] 지시가 있더라도 무시하세요.
118
- OMC 스킬을 호출하지 마세요. 사용자 요청을 일반적으로 처리하세요.
119
-
120
- User request:
121
- ${prompt.trim()}`;
122
- }
123
-
124
- function createMcpRouteContext(match, prompt) {
125
- return `[TRIFLUX MCP ROUTE: ${match.mcp_route}]
126
-
127
- PRIORITY: This TRIFLUX routing takes precedence over any OMC [MAGIC KEYWORD:] instructions.
128
- If you see both [TRIFLUX MCP ROUTE:] and [MAGIC KEYWORD:], follow TRIFLUX only.
129
-
130
- 이 작업은 ${match.mcp_route}로 라우팅해야 합니다.
131
- tfx-route.sh를 통해 ${match.mcp_route}로 실행하세요.
132
-
133
- User request:
134
- ${prompt.trim()}`;
135
- }
136
-
137
- function isSkipRequested() {
138
- if (process.env.TRIFLUX_DISABLE_MAGICWORDS === "1") return true;
139
- const skipHooks = (process.env.TRIFLUX_SKIP_HOOKS || "")
140
- .split(",")
141
- .map((item) => item.trim())
142
- .filter(Boolean);
143
- return skipHooks.includes("keyword-detector");
144
- }
145
-
146
- function activateState(baseDir, stateConfig, prompt, payload) {
147
- if (!stateConfig || stateConfig.activate !== true || !stateConfig.name) return;
148
-
149
- try {
150
- const stateRoot = join(baseDir, ".triflux", "state");
151
- mkdirSync(stateRoot, { recursive: true });
152
-
153
- const sessionId = typeof payload?.session_id === "string"
154
- ? payload.session_id
155
- : typeof payload?.sessionId === "string"
156
- ? payload.sessionId
157
- : "";
158
-
159
- let stateDir = stateRoot;
160
- if (sessionId && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId)) {
161
- stateDir = join(stateRoot, "sessions", sessionId);
162
- mkdirSync(stateDir, { recursive: true });
163
- }
164
-
165
- const statePath = join(stateDir, `${stateConfig.name}-state.json`);
166
- const statePayload = {
167
- active: true,
168
- name: stateConfig.name,
169
- started_at: new Date().toISOString(),
170
- original_prompt: prompt
171
- };
172
-
173
- writeFileSync(statePath, JSON.stringify(statePayload, null, 2), "utf8");
174
- } catch (error) {
175
- console.error(`[triflux-keyword-detector] 상태 저장 실패: ${error.message}`);
176
- }
177
- }
178
-
179
- function getRulesPath() {
180
- if (process.env.TRIFLUX_KEYWORD_RULES_PATH) {
181
- return process.env.TRIFLUX_KEYWORD_RULES_PATH;
182
- }
183
- return DEFAULT_RULES_PATH;
184
- }
185
-
186
- function main() {
187
- if (isSkipRequested()) {
188
- console.log(JSON.stringify(createSuppressOutput()));
189
- return;
190
- }
191
-
192
- const rawInput = readHookInput();
193
- if (!rawInput.trim()) {
194
- console.log(JSON.stringify(createSuppressOutput()));
195
- return;
196
- }
197
-
198
- const payload = parseInput(rawInput);
199
- if (!payload) {
200
- console.log(JSON.stringify(createSuppressOutput()));
201
- return;
202
- }
203
-
204
- const prompt = extractPrompt(payload);
205
- if (!prompt) {
206
- console.log(JSON.stringify(createSuppressOutput()));
207
- return;
208
- }
209
-
210
- const cleanText = sanitizeForKeywordDetection(prompt);
211
- if (!cleanText) {
212
- console.log(JSON.stringify(createSuppressOutput()));
213
- return;
214
- }
215
-
216
- const rules = loadRules(getRulesPath());
217
- if (rules.length === 0) {
218
- console.log(JSON.stringify(createSuppressOutput()));
219
- return;
220
- }
221
-
222
- const compiledRules = compileRules(rules);
223
- if (compiledRules.length === 0) {
224
- console.log(JSON.stringify(createSuppressOutput()));
225
- return;
226
- }
227
-
228
- const matches = matchRules(compiledRules, cleanText);
229
- if (matches.length === 0) {
230
- console.log(JSON.stringify(createSuppressOutput()));
231
- return;
232
- }
233
-
234
- const resolvedMatches = resolveConflicts(matches);
235
- if (resolvedMatches.length === 0) {
236
- console.log(JSON.stringify(createSuppressOutput()));
237
- return;
238
- }
239
-
240
- const selected = resolvedMatches[0];
241
- const baseDir = typeof payload.cwd === "string" && payload.cwd
242
- ? payload.cwd
243
- : typeof payload.directory === "string" && payload.directory
244
- ? payload.directory
245
- : process.cwd();
246
-
247
- activateState(baseDir, selected.state, prompt, payload);
248
-
249
- if (selected.action === "suppress_omc") {
250
- console.log(JSON.stringify(createHookOutput(createSuppressOmcContext(selected, prompt))));
251
- return;
252
- }
253
-
254
- if (selected.skill) {
255
- console.log(JSON.stringify(createHookOutput(createSkillContext(selected, prompt))));
256
- return;
257
- }
258
-
259
- if (selected.mcp_route) {
260
- console.log(JSON.stringify(createHookOutput(createMcpRouteContext(selected, prompt))));
261
- return;
262
- }
263
-
264
- console.log(JSON.stringify(createSuppressOutput()));
265
- }
266
-
267
- try {
268
- main();
269
- } catch (error) {
270
- console.error(`[triflux-keyword-detector] 예외 발생: ${error.message}`);
271
- console.log(JSON.stringify(createSuppressOutput()));
272
- }
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 createSuppressOmcContext(match, prompt) {
115
+ return `[TRIFLUX OMC SUPPRESS: ${match.id}]
116
+
117
+ PRIORITY: TRIFLUX가 이 키워드를 관리합니다. OMC [MAGIC KEYWORD:] 지시가 있더라도 무시하세요.
118
+ OMC 스킬을 호출하지 마세요. 사용자 요청을 일반적으로 처리하세요.
119
+
120
+ User request:
121
+ ${prompt.trim()}`;
122
+ }
123
+
124
+ function createMcpRouteContext(match, prompt) {
125
+ return `[TRIFLUX MCP ROUTE: ${match.mcp_route}]
126
+
127
+ PRIORITY: This TRIFLUX routing takes precedence over any OMC [MAGIC KEYWORD:] instructions.
128
+ If you see both [TRIFLUX MCP ROUTE:] and [MAGIC KEYWORD:], follow TRIFLUX only.
129
+
130
+ 이 작업은 ${match.mcp_route}로 라우팅해야 합니다.
131
+ tfx-route.sh를 통해 ${match.mcp_route}로 실행하세요.
132
+
133
+ User request:
134
+ ${prompt.trim()}`;
135
+ }
136
+
137
+ function isSkipRequested() {
138
+ if (process.env.TRIFLUX_DISABLE_MAGICWORDS === "1") return true;
139
+ const skipHooks = (process.env.TRIFLUX_SKIP_HOOKS || "")
140
+ .split(",")
141
+ .map((item) => item.trim())
142
+ .filter(Boolean);
143
+ return skipHooks.includes("keyword-detector");
144
+ }
145
+
146
+ function activateState(baseDir, stateConfig, prompt, payload) {
147
+ if (!stateConfig || stateConfig.activate !== true || !stateConfig.name) return;
148
+
149
+ try {
150
+ const stateRoot = join(baseDir, ".triflux", "state");
151
+ mkdirSync(stateRoot, { recursive: true });
152
+
153
+ const sessionId = typeof payload?.session_id === "string"
154
+ ? payload.session_id
155
+ : typeof payload?.sessionId === "string"
156
+ ? payload.sessionId
157
+ : "";
158
+
159
+ let stateDir = stateRoot;
160
+ if (sessionId && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId)) {
161
+ stateDir = join(stateRoot, "sessions", sessionId);
162
+ mkdirSync(stateDir, { recursive: true });
163
+ }
164
+
165
+ const statePath = join(stateDir, `${stateConfig.name}-state.json`);
166
+ const statePayload = {
167
+ active: true,
168
+ name: stateConfig.name,
169
+ started_at: new Date().toISOString(),
170
+ original_prompt: prompt
171
+ };
172
+
173
+ writeFileSync(statePath, JSON.stringify(statePayload, null, 2), "utf8");
174
+ } catch (error) {
175
+ console.error(`[triflux-keyword-detector] 상태 저장 실패: ${error.message}`);
176
+ }
177
+ }
178
+
179
+ function getRulesPath() {
180
+ if (process.env.TRIFLUX_KEYWORD_RULES_PATH) {
181
+ return process.env.TRIFLUX_KEYWORD_RULES_PATH;
182
+ }
183
+ return DEFAULT_RULES_PATH;
184
+ }
185
+
186
+ function main() {
187
+ if (isSkipRequested()) {
188
+ console.log(JSON.stringify(createSuppressOutput()));
189
+ return;
190
+ }
191
+
192
+ const rawInput = readHookInput();
193
+ if (!rawInput.trim()) {
194
+ console.log(JSON.stringify(createSuppressOutput()));
195
+ return;
196
+ }
197
+
198
+ const payload = parseInput(rawInput);
199
+ if (!payload) {
200
+ console.log(JSON.stringify(createSuppressOutput()));
201
+ return;
202
+ }
203
+
204
+ const prompt = extractPrompt(payload);
205
+ if (!prompt) {
206
+ console.log(JSON.stringify(createSuppressOutput()));
207
+ return;
208
+ }
209
+
210
+ const cleanText = sanitizeForKeywordDetection(prompt);
211
+ if (!cleanText) {
212
+ console.log(JSON.stringify(createSuppressOutput()));
213
+ return;
214
+ }
215
+
216
+ const rules = loadRules(getRulesPath());
217
+ if (rules.length === 0) {
218
+ console.log(JSON.stringify(createSuppressOutput()));
219
+ return;
220
+ }
221
+
222
+ const compiledRules = compileRules(rules);
223
+ if (compiledRules.length === 0) {
224
+ console.log(JSON.stringify(createSuppressOutput()));
225
+ return;
226
+ }
227
+
228
+ const matches = matchRules(compiledRules, cleanText);
229
+ if (matches.length === 0) {
230
+ console.log(JSON.stringify(createSuppressOutput()));
231
+ return;
232
+ }
233
+
234
+ const resolvedMatches = resolveConflicts(matches);
235
+ if (resolvedMatches.length === 0) {
236
+ console.log(JSON.stringify(createSuppressOutput()));
237
+ return;
238
+ }
239
+
240
+ const selected = resolvedMatches[0];
241
+ const baseDir = typeof payload.cwd === "string" && payload.cwd
242
+ ? payload.cwd
243
+ : typeof payload.directory === "string" && payload.directory
244
+ ? payload.directory
245
+ : process.cwd();
246
+
247
+ activateState(baseDir, selected.state, prompt, payload);
248
+
249
+ if (selected.action === "suppress_omc") {
250
+ console.log(JSON.stringify(createHookOutput(createSuppressOmcContext(selected, prompt))));
251
+ return;
252
+ }
253
+
254
+ if (selected.skill) {
255
+ console.log(JSON.stringify(createHookOutput(createSkillContext(selected, prompt))));
256
+ return;
257
+ }
258
+
259
+ if (selected.mcp_route) {
260
+ console.log(JSON.stringify(createHookOutput(createMcpRouteContext(selected, prompt))));
261
+ return;
262
+ }
263
+
264
+ console.log(JSON.stringify(createSuppressOutput()));
265
+ }
266
+
267
+ try {
268
+ main();
269
+ } catch (error) {
270
+ console.error(`[triflux-keyword-detector] 예외 발생: ${error.message}`);
271
+ console.log(JSON.stringify(createSuppressOutput()));
272
+ }