triflux 10.3.2 → 10.3.4

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 (65) hide show
  1. package/.claude-plugin/plugin.json +22 -22
  2. package/LICENSE +21 -21
  3. package/README.ko.md +16 -0
  4. package/README.md +8 -0
  5. package/hooks/hook-registry.json +256 -256
  6. package/hub/adaptive-inject.mjs +1 -1
  7. package/hub/assign-callbacks.mjs +120 -120
  8. package/hub/delegator/index.mjs +14 -14
  9. package/hub/delegator/tool-definitions.mjs +35 -35
  10. package/hub/hitl.mjs +143 -143
  11. package/hub/lib/path-utils.mjs +167 -0
  12. package/hub/router.mjs +791 -791
  13. package/hub/session-fingerprint.mjs +1 -1
  14. package/hub/team/cli/commands/attach.mjs +37 -37
  15. package/hub/team/cli/commands/debug.mjs +74 -74
  16. package/hub/team/cli/commands/focus.mjs +53 -53
  17. package/hub/team/cli/commands/list.mjs +24 -24
  18. package/hub/team/cli/commands/start/start-in-process.mjs +40 -40
  19. package/hub/team/cli/commands/start/start-mux.mjs +73 -73
  20. package/hub/team/cli/commands/start/start-wt.mjs +69 -69
  21. package/hub/team/cli/commands/tasks.mjs +13 -13
  22. package/hub/team/cli/render.mjs +30 -30
  23. package/hub/team/cli/services/attach-fallback.mjs +54 -54
  24. package/hub/team/cli/services/member-selector.mjs +30 -30
  25. package/hub/team/cli/services/native-control.mjs +116 -116
  26. package/hub/team/cli/services/task-model.mjs +30 -30
  27. package/hub/team/notify.mjs +1 -1
  28. package/hub/team/orchestrator.mjs +161 -161
  29. package/hub/team/runtime-strategy.mjs +74 -0
  30. package/hub/team/session.mjs +611 -611
  31. package/hub/team/shared.mjs +13 -13
  32. package/hub/team/worktree-lifecycle.mjs +61 -2
  33. package/hub/tray.mjs +368 -368
  34. package/hub/workers/codex-mcp.mjs +507 -507
  35. package/hub/workers/factory.mjs +21 -21
  36. package/hud/hud-qos-status.mjs +17 -3
  37. package/hud/mission-board.mjs +53 -0
  38. package/hud/providers/claude.mjs +95 -22
  39. package/hud/renderers.mjs +39 -5
  40. package/mesh/index.mjs +63 -0
  41. package/mesh/mesh-budget.mjs +128 -0
  42. package/mesh/mesh-heartbeat.mjs +100 -0
  43. package/mesh/mesh-protocol.mjs +96 -0
  44. package/mesh/mesh-queue.mjs +165 -0
  45. package/mesh/mesh-registry.mjs +78 -0
  46. package/mesh/mesh-router.mjs +76 -0
  47. package/package.json +2 -1
  48. package/scripts/completions/tfx.bash +47 -47
  49. package/scripts/completions/tfx.fish +44 -44
  50. package/scripts/completions/tfx.zsh +83 -83
  51. package/scripts/demo.mjs +169 -0
  52. package/scripts/headless-guard.mjs +16 -4
  53. package/scripts/hub-ensure.mjs +120 -120
  54. package/scripts/keyword-detector.mjs +272 -272
  55. package/scripts/keyword-rules-expander.mjs +521 -521
  56. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  57. package/scripts/lib/skill-state.mjs +220 -0
  58. package/scripts/notion-read.mjs +553 -553
  59. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  60. package/scripts/tfx-batch-stats.mjs +96 -96
  61. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +0 -1
  62. package/skills/.omc/state/idle-notif-cooldown.json +0 -3
  63. package/skills/.omc/state/last-tool-error.json +0 -7
  64. package/skills/.omc/state/subagent-tracking.json +0 -7
  65. package/skills/tfx-remote-spawn/references/hosts.json +0 -16
@@ -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
+ }