triflux 3.3.0-dev.8 → 4.0.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.
Files changed (91) hide show
  1. package/README.ko.md +108 -199
  2. package/README.md +108 -199
  3. package/bin/triflux.mjs +2427 -1762
  4. package/hooks/keyword-rules.json +361 -354
  5. package/hooks/pipeline-stop.mjs +5 -2
  6. package/hub/assign-callbacks.mjs +136 -136
  7. package/hub/bridge.mjs +734 -684
  8. package/hub/delegator/contracts.mjs +38 -38
  9. package/hub/delegator/index.mjs +14 -14
  10. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  11. package/hub/delegator/service.mjs +302 -118
  12. package/hub/delegator/tool-definitions.mjs +35 -35
  13. package/hub/hitl.mjs +67 -67
  14. package/hub/paths.mjs +28 -0
  15. package/hub/pipe.mjs +589 -561
  16. package/hub/pipeline/state.mjs +23 -0
  17. package/hub/public/dashboard.html +349 -0
  18. package/hub/public/tray-icon.ico +0 -0
  19. package/hub/public/tray-icon.png +0 -0
  20. package/hub/router.mjs +782 -782
  21. package/hub/schema.sql +40 -40
  22. package/hub/server.mjs +810 -637
  23. package/hub/store.mjs +706 -706
  24. package/hub/team/cli/commands/attach.mjs +37 -0
  25. package/hub/team/cli/commands/control.mjs +43 -0
  26. package/hub/team/cli/commands/debug.mjs +74 -0
  27. package/hub/team/cli/commands/focus.mjs +53 -0
  28. package/hub/team/cli/commands/interrupt.mjs +36 -0
  29. package/hub/team/cli/commands/kill.mjs +37 -0
  30. package/hub/team/cli/commands/list.mjs +24 -0
  31. package/hub/team/cli/commands/send.mjs +37 -0
  32. package/hub/team/cli/commands/start/index.mjs +87 -0
  33. package/hub/team/cli/commands/start/parse-args.mjs +32 -0
  34. package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
  35. package/hub/team/cli/commands/start/start-mux.mjs +73 -0
  36. package/hub/team/cli/commands/start/start-wt.mjs +69 -0
  37. package/hub/team/cli/commands/status.mjs +87 -0
  38. package/hub/team/cli/commands/stop.mjs +31 -0
  39. package/hub/team/cli/commands/task.mjs +30 -0
  40. package/hub/team/cli/commands/tasks.mjs +13 -0
  41. package/hub/team/{cli.mjs → cli/help.mjs} +38 -99
  42. package/hub/team/cli/index.mjs +39 -0
  43. package/hub/team/cli/manifest.mjs +28 -0
  44. package/hub/team/cli/render.mjs +30 -0
  45. package/hub/team/cli/services/attach-fallback.mjs +54 -0
  46. package/hub/team/cli/services/hub-client.mjs +171 -0
  47. package/hub/team/cli/services/member-selector.mjs +30 -0
  48. package/hub/team/cli/services/native-control.mjs +115 -0
  49. package/hub/team/cli/services/runtime-mode.mjs +60 -0
  50. package/hub/team/cli/services/state-store.mjs +34 -0
  51. package/hub/team/cli/services/task-model.mjs +30 -0
  52. package/hub/team/native-supervisor.mjs +69 -63
  53. package/hub/team/native.mjs +367 -367
  54. package/hub/team/nativeProxy.mjs +217 -173
  55. package/hub/team/pane.mjs +149 -149
  56. package/hub/team/psmux.mjs +946 -946
  57. package/hub/team/session.mjs +608 -608
  58. package/hub/team/staleState.mjs +369 -299
  59. package/hub/tools.mjs +107 -107
  60. package/hub/tray.mjs +332 -0
  61. package/hub/workers/claude-worker.mjs +446 -446
  62. package/hub/workers/codex-mcp.mjs +414 -414
  63. package/hub/workers/delegator-mcp.mjs +1045 -1045
  64. package/hub/workers/factory.mjs +21 -21
  65. package/hub/workers/gemini-worker.mjs +349 -349
  66. package/hub/workers/interface.mjs +41 -41
  67. package/package.json +3 -2
  68. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  69. package/scripts/hub-ensure.mjs +102 -101
  70. package/scripts/keyword-detector.mjs +272 -272
  71. package/scripts/keyword-rules-expander.mjs +521 -521
  72. package/scripts/lib/keyword-rules.mjs +168 -168
  73. package/scripts/lib/mcp-filter.mjs +642 -642
  74. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  75. package/scripts/mcp-check.mjs +126 -126
  76. package/scripts/preflight-cache.mjs +19 -0
  77. package/scripts/run.cjs +62 -62
  78. package/scripts/setup.mjs +68 -31
  79. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  80. package/scripts/tfx-route-worker.mjs +161 -161
  81. package/scripts/tfx-route.sh +1360 -1326
  82. package/skills/tfx-auto/SKILL.md +196 -196
  83. package/skills/tfx-auto-codex/SKILL.md +77 -77
  84. package/skills/tfx-multi/SKILL.md +378 -378
  85. package/hub/team/cli-team-common.mjs +0 -348
  86. package/hub/team/cli-team-control.mjs +0 -393
  87. package/hub/team/cli-team-start.mjs +0 -516
  88. package/hub/team/cli-team-status.mjs +0 -283
  89. package/skills/auto-verify/SKILL.md +0 -145
  90. package/skills/manage-skills/SKILL.md +0 -192
  91. package/skills/verify-implementation/SKILL.md +0 -138
@@ -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
+ }