@triflux/core 10.1.0 → 10.26.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 (145) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +20 -8
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +267 -35
  7. package/hooks/hook-registry.json +343 -256
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +191 -31
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +262 -34
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +1051 -290
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -4
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +22 -21
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/schema/delegator-tools.schema.json +1 -1
  35. package/hub/delegator/service.mjs +93 -64
  36. package/hub/delegator/tool-definitions.mjs +35 -35
  37. package/hub/dynamic-routing-engine.mjs +511 -0
  38. package/hub/fullcycle.mjs +34 -18
  39. package/hub/gemini-adapter.mjs +82 -108
  40. package/hub/hitl.mjs +202 -143
  41. package/hub/intent.mjs +167 -38
  42. package/hub/lib/bash-path.mjs +73 -0
  43. package/hub/lib/cache-guard.mjs +43 -17
  44. package/hub/lib/env-detect.mjs +145 -0
  45. package/hub/lib/hosts-compat.mjs +319 -0
  46. package/hub/lib/mcp-response-cache.mjs +239 -0
  47. package/hub/lib/memory-store.mjs +285 -111
  48. package/hub/lib/path-utils.mjs +165 -0
  49. package/hub/lib/phase-manager.mjs +493 -0
  50. package/hub/lib/process-utils.mjs +797 -104
  51. package/hub/lib/prompt-tmp.mjs +33 -0
  52. package/hub/lib/risk-tier.mjs +53 -0
  53. package/hub/lib/spawn-trace.mjs +561 -0
  54. package/hub/lib/ssh-command.mjs +44 -24
  55. package/hub/lib/ssh-retry.mjs +5 -1
  56. package/hub/lib/staged-file-detect.mjs +43 -0
  57. package/hub/lib/state-snapshot.mjs +293 -0
  58. package/hub/lib/tfx-route-args.mjs +309 -0
  59. package/hub/lib/timeline-adapter.mjs +190 -0
  60. package/hub/lib/trace-recorder.mjs +153 -0
  61. package/hub/lib/uuidv7.mjs +4 -3
  62. package/hub/memory-doctor.mjs +266 -106
  63. package/hub/middleware/quota-middleware.mjs +70 -0
  64. package/hub/middleware/request-logger.mjs +61 -34
  65. package/hub/paths.mjs +9 -9
  66. package/hub/pipeline/gates/confidence.mjs +34 -15
  67. package/hub/pipeline/gates/consensus.mjs +27 -15
  68. package/hub/pipeline/gates/index.mjs +7 -3
  69. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  70. package/hub/pipeline/index.mjs +77 -42
  71. package/hub/pipeline/state.mjs +10 -10
  72. package/hub/pipeline/transitions.mjs +40 -23
  73. package/hub/platform.mjs +65 -48
  74. package/hub/quality/deslop.mjs +70 -49
  75. package/hub/reflexion.mjs +87 -13
  76. package/hub/research.mjs +33 -25
  77. package/hub/router.mjs +941 -791
  78. package/hub/routing/complexity.mjs +132 -29
  79. package/hub/routing/index.mjs +17 -12
  80. package/hub/routing/q-learning.mjs +78 -30
  81. package/hub/routing-snapshot.mjs +261 -0
  82. package/hub/session-fingerprint.mjs +127 -61
  83. package/hub/state.mjs +86 -44
  84. package/hub/team/claude-daemon-control.mjs +1130 -0
  85. package/hub/team/retry-state-machine.mjs +336 -0
  86. package/hub/token-mode.mjs +72 -71
  87. package/hub/workers/worker-utils.mjs +23 -17
  88. package/hud/colors.mjs +27 -9
  89. package/hud/constants.mjs +190 -27
  90. package/hud/context-monitor.mjs +133 -44
  91. package/hud/hud-qos-status.mjs +157 -50
  92. package/hud/mission-board.mjs +56 -0
  93. package/hud/providers/claude.mjs +398 -114
  94. package/hud/providers/codex.mjs +96 -21
  95. package/hud/providers/gemini.mjs +210 -52
  96. package/hud/renderers.mjs +440 -127
  97. package/hud/terminal.mjs +101 -31
  98. package/hud/utils.mjs +81 -39
  99. package/mesh/index.mjs +69 -0
  100. package/mesh/mesh-budget.mjs +137 -0
  101. package/mesh/mesh-heartbeat.mjs +100 -0
  102. package/mesh/mesh-protocol.mjs +96 -0
  103. package/mesh/mesh-queue.mjs +163 -0
  104. package/mesh/mesh-registry.mjs +78 -0
  105. package/mesh/mesh-router.mjs +77 -0
  106. package/package.json +5 -1
  107. package/scripts/lib/agent-json.mjs +27 -0
  108. package/scripts/lib/async.mjs +174 -0
  109. package/scripts/lib/claudemd-scanner.mjs +6 -1
  110. package/scripts/lib/cli-agy.mjs +62 -0
  111. package/scripts/lib/cli-claude.mjs +78 -0
  112. package/scripts/lib/cli-codex.mjs +199 -0
  113. package/scripts/lib/cli-gemini.mjs +37 -0
  114. package/scripts/lib/codex-recovery.sh +50 -0
  115. package/scripts/lib/context.mjs +3 -3
  116. package/scripts/lib/cross-review-utils.mjs +13 -5
  117. package/scripts/lib/doctor-env-checks.mjs +121 -0
  118. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  119. package/scripts/lib/env-probe.mjs +58 -30
  120. package/scripts/lib/env.mjs +14 -0
  121. package/scripts/lib/gemini-profiles.mjs +44 -10
  122. package/scripts/lib/handoff.mjs +33 -17
  123. package/scripts/lib/hook-utils.mjs +8 -6
  124. package/scripts/lib/hub.mjs +48 -0
  125. package/scripts/lib/keyword-rules.mjs +49 -20
  126. package/scripts/lib/logger.mjs +24 -24
  127. package/scripts/lib/mcp-filter.mjs +391 -247
  128. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  129. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  130. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  131. package/scripts/lib/mcp-guard-engine.mjs +1281 -115
  132. package/scripts/lib/mcp-health.mjs +773 -0
  133. package/scripts/lib/mcp-manifest.mjs +24 -14
  134. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  135. package/scripts/lib/pid.mjs +63 -0
  136. package/scripts/lib/process-utils.mjs +26 -0
  137. package/scripts/lib/psmux-info.mjs +117 -12
  138. package/scripts/lib/quota.mjs +47 -0
  139. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  140. package/scripts/lib/skill-state.mjs +220 -0
  141. package/scripts/lib/skill-template.mjs +32 -7
  142. package/scripts/lib/team.mjs +75 -0
  143. package/scripts/lib/timeout.mjs +66 -0
  144. package/scripts/lib/tmp.mjs +26 -0
  145. package/scripts/lib/toml.mjs +113 -0
package/hooks/hooks.json CHANGED
@@ -25,6 +25,18 @@
25
25
  ]
26
26
  }
27
27
  ],
28
+ "PermissionRequest": [
29
+ {
30
+ "matcher": "Bash",
31
+ "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-orchestrator.mjs\"",
35
+ "timeout": 3
36
+ }
37
+ ]
38
+ }
39
+ ],
28
40
  "PreToolUse": [
29
41
  {
30
42
  "matcher": "Bash|Agent|Skill",
@@ -39,7 +51,7 @@
39
51
  ],
40
52
  "PostToolUse": [
41
53
  {
42
- "matcher": "Edit|Write",
54
+ "matcher": "Bash|Edit|Write",
43
55
  "hooks": [
44
56
  {
45
57
  "type": "command",
@@ -73,6 +85,30 @@
73
85
  ]
74
86
  }
75
87
  ],
88
+ "SessionEnd": [
89
+ {
90
+ "matcher": "*",
91
+ "hooks": [
92
+ {
93
+ "type": "command",
94
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-orchestrator.mjs\"",
95
+ "timeout": 5
96
+ }
97
+ ]
98
+ }
99
+ ],
100
+ "SubagentStart": [
101
+ {
102
+ "matcher": "*",
103
+ "hooks": [
104
+ {
105
+ "type": "command",
106
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-orchestrator.mjs\"",
107
+ "timeout": 5
108
+ }
109
+ ]
110
+ }
111
+ ],
76
112
  "SubagentStop": [
77
113
  {
78
114
  "matcher": "*",
@@ -84,6 +120,18 @@
84
120
  }
85
121
  ]
86
122
  }
123
+ ],
124
+ "PreCompact": [
125
+ {
126
+ "matcher": "*",
127
+ "hooks": [
128
+ {
129
+ "type": "command",
130
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-orchestrator.mjs\"",
131
+ "timeout": 5
132
+ }
133
+ ]
134
+ }
87
135
  ]
88
136
  }
89
137
  }
@@ -11,12 +11,7 @@
11
11
  "skill": null,
12
12
  "action": "suppress_all",
13
13
  "priority": 0,
14
- "supersedes": [
15
- "tfx-multi",
16
- "tfx-unified",
17
- "tfx-codex",
18
- "tfx-gemini"
19
- ],
14
+ "supersedes": ["tfx-multi", "tfx-unified", "tfx-codex", "tfx-gemini"],
20
15
  "exclusive": true,
21
16
  "state": null,
22
17
  "mcp_route": null
@@ -29,7 +24,7 @@
29
24
  "flags": "i"
30
25
  }
31
26
  ],
32
- "skill": "tfx-multi",
27
+ "skill": "tfx-auto",
33
28
  "priority": 1,
34
29
  "supersedes": [],
35
30
  "exclusive": false,
@@ -62,9 +57,7 @@
62
57
  ],
63
58
  "skill": "tfx-swarm",
64
59
  "priority": 1,
65
- "supersedes": [
66
- "tfx-codex"
67
- ],
60
+ "supersedes": ["tfx-codex"],
68
61
  "exclusive": false,
69
62
  "state": null,
70
63
  "mcp_route": null
@@ -72,15 +65,54 @@
72
65
  {
73
66
  "id": "tfx-unified",
74
67
  "patterns": [
75
- { "source": "\\btfx[\\s-]?auto\\b", "flags": "i" },
76
- { "source": "\\btfx[\\s-]?auto[\\s-]?codex\\b", "flags": "i" },
77
- { "source": "(?:만들어|고쳐|구현해|짜줘|수정해|바꿔)", "flags": "i" },
78
- { "source": "(?:리뷰해|검토해|봐줘|괜찮아)", "flags": "i" },
79
- { "source": "(?:테스트|검증|돌려봐|QA)", "flags": "i" },
80
- { "source": "(?:분석해|계획|설계해)", "flags": "i" },
81
- { "source": "(?:찾아봐|조사해|검색해)", "flags": "i" },
82
- { "source": "(?:정리해|슬롭|클린업)", "flags": "i" },
83
- { "source": "\\b(?:implement|build|fix|review|test|plan|analyze)\\b", "flags": "i" }
68
+ {
69
+ "source": "\\btfx[\\s-]?auto\\b",
70
+ "flags": "i"
71
+ },
72
+ {
73
+ "source": "\\btfx[\\s-]?auto[\\s-]?codex\\b",
74
+ "flags": "i"
75
+ },
76
+ {
77
+ "source": "(?:만들어|고쳐|구현해|짜줘|수정해|바꿔)",
78
+ "flags": "i"
79
+ },
80
+ {
81
+ "source": "(?:리뷰해|검토해|봐줘|괜찮아)",
82
+ "flags": "i"
83
+ },
84
+ {
85
+ "source": "(?:테스트|검증|돌려봐|QA)",
86
+ "flags": "i"
87
+ },
88
+ {
89
+ "source": "(?:분석해|계획|설계해)",
90
+ "flags": "i"
91
+ },
92
+ {
93
+ "source": "(?:찾아봐|조사해|검색해)",
94
+ "flags": "i"
95
+ },
96
+ {
97
+ "source": "(?:정리해|슬롭|클린업)",
98
+ "flags": "i"
99
+ },
100
+ {
101
+ "source": "(?:병렬|동시에|parallel|concurrent)",
102
+ "flags": "i"
103
+ },
104
+ {
105
+ "source": "(?:점검|진단|확인해)",
106
+ "flags": "i"
107
+ },
108
+ {
109
+ "source": "(?:계속해|이어서|진행해)",
110
+ "flags": "i"
111
+ },
112
+ {
113
+ "source": "\\b(?:implement|build|fix|review|test|plan|analyze)\\b",
114
+ "flags": "i"
115
+ }
84
116
  ],
85
117
  "skill": "tfx-auto",
86
118
  "priority": 2,
@@ -97,7 +129,7 @@
97
129
  "flags": "i"
98
130
  }
99
131
  ],
100
- "skill": "tfx-codex",
132
+ "skill": "tfx-auto",
101
133
  "priority": 3,
102
134
  "supersedes": [],
103
135
  "exclusive": false,
@@ -112,7 +144,7 @@
112
144
  "flags": "i"
113
145
  }
114
146
  ],
115
- "skill": "tfx-gemini",
147
+ "skill": "tfx-auto",
116
148
  "priority": 3,
117
149
  "supersedes": [],
118
150
  "exclusive": false,
@@ -172,7 +204,7 @@
172
204
  "flags": "i"
173
205
  }
174
206
  ],
175
- "skill": "tfx-autoresearch",
207
+ "skill": "tfx-research",
176
208
  "priority": 2,
177
209
  "supersedes": [],
178
210
  "exclusive": false,
@@ -187,13 +219,101 @@
187
219
  "flags": "i"
188
220
  }
189
221
  ],
190
- "skill": "tfx-deep-interview",
222
+ "skill": "tfx-interview",
191
223
  "priority": 2,
192
224
  "supersedes": [],
193
225
  "exclusive": false,
194
226
  "state": null,
195
227
  "mcp_route": null
196
228
  },
229
+ {
230
+ "id": "wt-tab-route",
231
+ "patterns": [
232
+ {
233
+ "source": "(?:새\\s*탭|탭\\s*(?:새로|추가|생성|열|띄|파|만들))",
234
+ "flags": "i"
235
+ },
236
+ {
237
+ "source": "(?:패인|화면|pane)\\s*(?:분할|나눠|스플릿|split)",
238
+ "flags": "i"
239
+ },
240
+ {
241
+ "source": "\\bwt\\b\\s*(?:에\\s*)?(?:탭|tab|열|띄|새)",
242
+ "flags": "i"
243
+ },
244
+ {
245
+ "source": "(?:터미널|terminal)\\s*(?:탭|새\\s*탭)",
246
+ "flags": "i"
247
+ },
248
+ {
249
+ "source": "\\b(?:new\\s+tab|split\\s+pane|open\\s+(?:new\\s+)?tab)\\b",
250
+ "flags": "i"
251
+ }
252
+ ],
253
+ "skill": "tfx-wt",
254
+ "priority": 1,
255
+ "supersedes": ["tfx-unified"],
256
+ "exclusive": false,
257
+ "state": null,
258
+ "mcp_route": null
259
+ },
260
+ {
261
+ "id": "wt-tab-rename",
262
+ "patterns": [
263
+ {
264
+ "source": "(?:탭\\s*(?:이름|제목)\\s*(?:바꿔|변경|rename))",
265
+ "flags": "i"
266
+ },
267
+ {
268
+ "source": "(?:rename\\s+tab|tab\\s+rename)",
269
+ "flags": "i"
270
+ }
271
+ ],
272
+ "skill": "tfx-wt",
273
+ "priority": 1,
274
+ "supersedes": ["tfx-unified"],
275
+ "exclusive": false,
276
+ "state": null,
277
+ "mcp_route": null
278
+ },
279
+ {
280
+ "id": "wt-tab-list",
281
+ "patterns": [
282
+ {
283
+ "source": "(?:탭\\s*(?:목록|리스트|열린|현재))",
284
+ "flags": "i"
285
+ },
286
+ {
287
+ "source": "(?:list\\s+tabs|tab\\s+list|열린\\s*탭)",
288
+ "flags": "i"
289
+ }
290
+ ],
291
+ "skill": "tfx-wt",
292
+ "priority": 1,
293
+ "supersedes": ["tfx-unified"],
294
+ "exclusive": false,
295
+ "state": null,
296
+ "mcp_route": null
297
+ },
298
+ {
299
+ "id": "wt-tab-close",
300
+ "patterns": [
301
+ {
302
+ "source": "(?:탭\\s*(?:닫아|닫기|종료|정리|close))",
303
+ "flags": "i"
304
+ },
305
+ {
306
+ "source": "(?:close\\s+tab|tab\\s+close|탭\\s*정리)",
307
+ "flags": "i"
308
+ }
309
+ ],
310
+ "skill": "tfx-wt",
311
+ "priority": 1,
312
+ "supersedes": ["tfx-unified"],
313
+ "exclusive": false,
314
+ "state": null,
315
+ "mcp_route": null
316
+ },
197
317
  {
198
318
  "id": "handoff-route",
199
319
  "patterns": [
@@ -227,7 +347,7 @@
227
347
  "supersedes": [],
228
348
  "exclusive": false,
229
349
  "state": null,
230
- "mcp_route": "gemini"
350
+ "mcp_route": "antigravity"
231
351
  },
232
352
  {
233
353
  "id": "chrome-route",
@@ -246,7 +366,7 @@
246
366
  "supersedes": [],
247
367
  "exclusive": false,
248
368
  "state": null,
249
- "mcp_route": "gemini"
369
+ "mcp_route": "antigravity"
250
370
  },
251
371
  {
252
372
  "id": "jira-route",
@@ -284,7 +404,7 @@
284
404
  "supersedes": [],
285
405
  "exclusive": false,
286
406
  "state": null,
287
- "mcp_route": "gemini"
407
+ "mcp_route": "antigravity"
288
408
  },
289
409
  {
290
410
  "id": "calendar-route",
@@ -303,7 +423,7 @@
303
423
  "supersedes": [],
304
424
  "exclusive": false,
305
425
  "state": null,
306
- "mcp_route": "gemini"
426
+ "mcp_route": "antigravity"
307
427
  },
308
428
  {
309
429
  "id": "playwright-route",
@@ -318,7 +438,7 @@
318
438
  "supersedes": [],
319
439
  "exclusive": false,
320
440
  "state": null,
321
- "mcp_route": "gemini"
441
+ "mcp_route": "antigravity"
322
442
  },
323
443
  {
324
444
  "id": "canva-route",
@@ -333,7 +453,7 @@
333
453
  "supersedes": [],
334
454
  "exclusive": false,
335
455
  "state": null,
336
- "mcp_route": "gemini"
456
+ "mcp_route": "antigravity"
337
457
  },
338
458
  {
339
459
  "id": "confluence-route",
@@ -352,7 +472,7 @@
352
472
  "supersedes": [],
353
473
  "exclusive": false,
354
474
  "state": null,
355
- "mcp_route": "gemini"
475
+ "mcp_route": "antigravity"
356
476
  },
357
477
  {
358
478
  "id": "slack-route",
@@ -371,7 +491,7 @@
371
491
  "supersedes": [],
372
492
  "exclusive": false,
373
493
  "state": null,
374
- "mcp_route": "gemini"
494
+ "mcp_route": "antigravity"
375
495
  },
376
496
  {
377
497
  "id": "github-route",
@@ -432,6 +552,7 @@
432
552
  },
433
553
  {
434
554
  "id": "gstack-ship",
555
+ "disabled": true,
435
556
  "patterns": [
436
557
  {
437
558
  "source": "(?:배포해|PR\\s*만들|릴리스\\s*해|머지하고\\s*배포)",
@@ -555,6 +676,45 @@
555
676
  "exclusive": false,
556
677
  "state": null,
557
678
  "mcp_route": null
679
+ },
680
+ {
681
+ "id": "tfx-ship",
682
+ "patterns": [
683
+ {
684
+ "source": "\\btfx[\\s-]?ship\\b",
685
+ "flags": "i"
686
+ },
687
+ {
688
+ "source": "/ship\\b",
689
+ "flags": "i"
690
+ },
691
+ {
692
+ "source": "\\brelease\\b",
693
+ "flags": "i"
694
+ },
695
+ {
696
+ "source": "\\bpublish\\b",
697
+ "flags": "i"
698
+ },
699
+ {
700
+ "source": "배포(?!자|사|장|처)",
701
+ "flags": ""
702
+ },
703
+ {
704
+ "source": "릴리[즈스]",
705
+ "flags": ""
706
+ },
707
+ {
708
+ "source": "쉽\\s*(?:해|하자|가자|쳐)",
709
+ "flags": ""
710
+ }
711
+ ],
712
+ "skill": "tfx-ship",
713
+ "priority": 3,
714
+ "supersedes": ["gstack-ship"],
715
+ "exclusive": false,
716
+ "state": null,
717
+ "mcp_route": null
558
718
  }
559
719
  ]
560
720
  }
@@ -51,7 +51,7 @@ export function resolvePluginRoot(callerUrl) {
51
51
 
52
52
  const moduleFallback = toPluginRootFromUrl(import.meta.url) || process.cwd();
53
53
  process.stderr.write(
54
- `[resolve-root] warning: failed to resolve plugin root from breadcrumb/env/caller; fallback=${moduleFallback}\n`
54
+ `[resolve-root] warning: failed to resolve plugin root from breadcrumb/env/caller; fallback=${moduleFallback}\n`,
55
55
  );
56
56
  return moduleFallback;
57
57
  }
@@ -25,10 +25,14 @@ function buildSystemMessage(filePath, stdioServers, result) {
25
25
 
26
26
  if (result.modified) {
27
27
  const actionLabel = result.replacement ? "자동 치환" : "자동 제거";
28
- lines.push(`[mcp-guard] stdio MCP ${actionLabel}: ${stdioServers.map((server) => server.name).join(", ")}`);
28
+ lines.push(
29
+ `[mcp-guard] stdio MCP ${actionLabel}: ${stdioServers.map((server) => server.name).join(", ")}`,
30
+ );
29
31
 
30
32
  if (result.replacement?.name && result.replacement?.url) {
31
- lines.push(`[mcp-guard] 대체 서버: ${result.replacement.name} -> ${result.replacement.url}`);
33
+ lines.push(
34
+ `[mcp-guard] 대체 서버: ${result.replacement.name} -> ${result.replacement.url}`,
35
+ );
32
36
  }
33
37
 
34
38
  if (result.backupPath) {
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { stdin, stdout } from "node:process";
4
+
5
+ const META_PATTERNS = [/&&/, /\|\|/, /[;|<>`]/, /\$\(/];
6
+ const RISKY_PATTERNS = [
7
+ /(^|\s)rm(\s|$)/,
8
+ /(^|\s)mv(\s|$)/,
9
+ /(^|\s)cp(\s|$)/,
10
+ /(^|\s)curl(\s|$)/,
11
+ /(^|\s)wget(\s|$)/,
12
+ /(^|\s)sudo(\s|$)/,
13
+ /(^|\s)chmod(\s|$)/,
14
+ /(^|\s)chown(\s|$)/,
15
+ /(^|\s)git\s+push(\s|$)/,
16
+ /(^|\s)git\s+reset(\s|$)/,
17
+ /(^|\s)git\s+clean(\s|$)/,
18
+ /(^|\s)npm\s+publish(\s|$)/,
19
+ /(^|\s)pnpm\s+publish(\s|$)/,
20
+ /(^|\s)yarn\s+publish(\s|$)/,
21
+ ];
22
+
23
+ function silentlyExit() {
24
+ process.exit(0);
25
+ }
26
+
27
+ function readStdin() {
28
+ return new Promise((resolve) => {
29
+ let input = "";
30
+ stdin.setEncoding("utf8");
31
+ stdin.on("data", (chunk) => {
32
+ input += chunk;
33
+ });
34
+ stdin.on("end", () => resolve(input));
35
+ stdin.resume();
36
+ });
37
+ }
38
+
39
+ function shellWords(command) {
40
+ const words = [];
41
+ const pattern = /"([^"]*)"|'([^']*)'|([^\s]+)/g;
42
+ let match;
43
+ while ((match = pattern.exec(command)) !== null) {
44
+ words.push(match[1] ?? match[2] ?? match[3]);
45
+ }
46
+ return words;
47
+ }
48
+
49
+ function isRepoRelativePath(value) {
50
+ if (!value || value.startsWith("/") || value.startsWith("~")) return false;
51
+ if (value === ".") return true;
52
+ if (value === ".." || value.startsWith("../") || value.includes("/../"))
53
+ return false;
54
+ return /^[A-Za-z0-9._/*?@{}+,:=-][A-Za-z0-9._/*?'@{}+,:=-]*$/.test(value);
55
+ }
56
+
57
+ function hasUnsafeSurface(command) {
58
+ return (
59
+ META_PATTERNS.some((pattern) => pattern.test(command)) ||
60
+ RISKY_PATTERNS.some((pattern) => pattern.test(command))
61
+ );
62
+ }
63
+
64
+ function allowGit(words, command) {
65
+ if (command === "git status" || command === "git status --short")
66
+ return "git status";
67
+ if (command === "git diff --stat" || command === "git diff --cached --stat")
68
+ return "git diff stat";
69
+ if (command === "git branch --show-current") return "git branch";
70
+ if (words[0] === "git" && words[1] === "log" && words[2] === "--oneline") {
71
+ const rest = words.slice(3);
72
+ if (
73
+ rest.every(
74
+ (word) =>
75
+ /^[A-Za-z0-9._/@{}~^:+,=-]+$/.test(word) && !word.startsWith("-"),
76
+ )
77
+ ) {
78
+ return "git log";
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+
84
+ function allowListing(words) {
85
+ if (words[0] === "pwd" && words.length === 1) return "pwd";
86
+
87
+ if (words[0] === "ls") {
88
+ const args = words.slice(1);
89
+ if (
90
+ args.every(
91
+ (word) => /^-[A-Za-z0-9]+$/.test(word) || isRepoRelativePath(word),
92
+ )
93
+ ) {
94
+ return "ls";
95
+ }
96
+ }
97
+
98
+ if (words[0] === "find") {
99
+ const args = words.slice(1);
100
+ if (args.some((word) => word === "-delete" || word.startsWith("-exec")))
101
+ return null;
102
+ const firstOption = args.findIndex((word) => word.startsWith("-"));
103
+ const pathArgs = (
104
+ firstOption === -1 ? args : args.slice(0, firstOption)
105
+ ).filter(Boolean);
106
+ if (pathArgs.length === 0 || pathArgs.every(isRepoRelativePath)) {
107
+ return "find";
108
+ }
109
+ }
110
+
111
+ return null;
112
+ }
113
+
114
+ function allowSearch(words) {
115
+ if (words[0] !== "rg" && words[0] !== "grep") return null;
116
+ const args = words.slice(1);
117
+ if (args.length === 0) return null;
118
+ if (
119
+ args.some(
120
+ (word) =>
121
+ word.startsWith("/") ||
122
+ word === ".." ||
123
+ word.startsWith("../") ||
124
+ word.includes("/../"),
125
+ )
126
+ ) {
127
+ return null;
128
+ }
129
+ return `${words[0]} search`;
130
+ }
131
+
132
+ function allowTargetedTest(words) {
133
+ if (words[0] !== "node" || words[1] !== "scripts/test-lock.mjs") return null;
134
+ if (!words.includes("--test")) return null;
135
+ const args = words.slice(2);
136
+ const allowedFlags = new Set(["--test", "--test-force-exit"]);
137
+ const pathArgs = [];
138
+ for (const word of args) {
139
+ if (allowedFlags.has(word)) continue;
140
+ if (/^--test-concurrency=\d+$/.test(word)) continue;
141
+ if (word.startsWith("-")) return null;
142
+ pathArgs.push(word);
143
+ }
144
+ if (pathArgs.length === 0) return null;
145
+ if (
146
+ pathArgs.every(
147
+ (word) =>
148
+ word.startsWith("tests/") || word.startsWith("scripts/__tests__/"),
149
+ ) &&
150
+ pathArgs.every(isRepoRelativePath)
151
+ ) {
152
+ return "targeted test";
153
+ }
154
+ return null;
155
+ }
156
+
157
+ function allowNpm(command) {
158
+ // npm scripts are intentionally not auto-allowed: package.json can redefine
159
+ // them to perform arbitrary side effects. Prefer explicit node test commands.
160
+ void command;
161
+ return null;
162
+ }
163
+
164
+ function allowanceReason(command) {
165
+ const trimmed = command.trim().replace(/\s+/g, " ");
166
+ if (!trimmed || hasUnsafeSurface(trimmed)) return null;
167
+
168
+ const words = shellWords(trimmed);
169
+ if (words.length === 0) return null;
170
+
171
+ return (
172
+ allowGit(words, trimmed) ??
173
+ allowListing(words) ??
174
+ allowSearch(words) ??
175
+ allowTargetedTest(words) ??
176
+ allowNpm(trimmed)
177
+ );
178
+ }
179
+
180
+ function hasAskSuggestion(value) {
181
+ if (!value || typeof value !== "object") return false;
182
+ if (Array.isArray(value)) return value.some(hasAskSuggestion);
183
+ if (value.behavior === "ask") return true;
184
+ if (value.decision?.behavior === "ask") return true;
185
+ return Object.values(value).some(hasAskSuggestion);
186
+ }
187
+
188
+ function emitAllow(reason) {
189
+ stdout.write(
190
+ JSON.stringify({
191
+ hookSpecificOutput: {
192
+ hookEventName: "PermissionRequest",
193
+ decision: { behavior: "allow" },
194
+ additionalContext: `[permission-safe-allow] allowed: ${reason}`,
195
+ },
196
+ }),
197
+ );
198
+ }
199
+
200
+ const input = await readStdin();
201
+ let payload;
202
+ try {
203
+ payload = JSON.parse(input || "{}");
204
+ } catch {
205
+ silentlyExit();
206
+ }
207
+
208
+ if (
209
+ payload?.hook_event_name !== "PermissionRequest" ||
210
+ payload?.tool_name !== "Bash"
211
+ ) {
212
+ silentlyExit();
213
+ }
214
+
215
+ if (
216
+ hasAskSuggestion(
217
+ payload.permission_suggestions ?? payload.permissionSuggestions,
218
+ )
219
+ ) {
220
+ silentlyExit();
221
+ }
222
+
223
+ const command = payload?.tool_input?.command ?? payload?.command;
224
+ if (typeof command !== "string") {
225
+ silentlyExit();
226
+ }
227
+
228
+ const reason = allowanceReason(command);
229
+ if (!reason) {
230
+ silentlyExit();
231
+ }
232
+
233
+ emitAllow(reason);