triflux 10.3.4 → 10.7.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 (329) hide show
  1. package/LICENSE +21 -21
  2. package/bin/tfx-doctor-tui.mjs +1 -1
  3. package/bin/tfx-doctor.mjs +6 -1
  4. package/bin/tfx-profile.mjs +1 -1
  5. package/bin/tfx-setup-tui.mjs +1 -1
  6. package/bin/tfx-setup.mjs +6 -1
  7. package/bin/triflux.mjs +2396 -1140
  8. package/hooks/agent-route-guard.mjs +12 -8
  9. package/hooks/cross-review-tracker.mjs +21 -8
  10. package/hooks/error-context.mjs +19 -7
  11. package/hooks/hook-adaptive-collector.mjs +18 -16
  12. package/hooks/hook-manager.mjs +93 -32
  13. package/hooks/hook-orchestrator.mjs +108 -24
  14. package/hooks/hook-registry.json +11 -0
  15. package/hooks/keyword-rules.json +6 -10
  16. package/hooks/lib/resolve-root.mjs +1 -1
  17. package/hooks/mcp-config-watcher.mjs +6 -2
  18. package/hooks/pipeline-stop.mjs +3 -6
  19. package/hooks/safety-guard.mjs +99 -28
  20. package/hooks/session-start-fast.mjs +143 -0
  21. package/hooks/subagent-verifier.mjs +5 -4
  22. package/hub/account-broker.mjs +256 -60
  23. package/hub/adaptive-diagnostic.mjs +75 -48
  24. package/hub/adaptive-inject.mjs +95 -57
  25. package/hub/adaptive-memory.mjs +156 -42
  26. package/hub/adaptive.mjs +60 -31
  27. package/hub/assign-callbacks.mjs +67 -30
  28. package/hub/bridge.mjs +0 -1
  29. package/hub/cli-adapter-base.mjs +200 -48
  30. package/hub/codex-adapter.mjs +76 -96
  31. package/hub/codex-compat.mjs +3 -3
  32. package/hub/codex-preflight.mjs +63 -37
  33. package/hub/delegator/contracts.mjs +19 -23
  34. package/hub/delegator/index.mjs +3 -3
  35. package/hub/delegator/service.mjs +88 -64
  36. package/hub/delegator/tool-definitions.mjs +5 -5
  37. package/hub/fullcycle.mjs +33 -17
  38. package/hub/gemini-adapter.mjs +69 -94
  39. package/hub/hitl.mjs +89 -30
  40. package/hub/intent.mjs +161 -38
  41. package/hub/lib/cache-guard.mjs +43 -17
  42. package/hub/lib/mcp-response-cache.mjs +66 -32
  43. package/hub/lib/memory-store.mjs +285 -111
  44. package/hub/lib/path-utils.mjs +35 -37
  45. package/hub/lib/process-utils.mjs +106 -37
  46. package/hub/lib/spawn-trace.mjs +527 -0
  47. package/hub/lib/ssh-command.mjs +34 -4
  48. package/hub/lib/ssh-retry.mjs +5 -1
  49. package/hub/lib/uuidv7.mjs +4 -3
  50. package/hub/memory-doctor.mjs +266 -106
  51. package/hub/middleware/request-logger.mjs +61 -34
  52. package/hub/paths.mjs +9 -9
  53. package/hub/pipeline/gates/confidence.mjs +34 -15
  54. package/hub/pipeline/gates/consensus.mjs +27 -15
  55. package/hub/pipeline/gates/index.mjs +7 -3
  56. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  57. package/hub/pipeline/index.mjs +77 -42
  58. package/hub/pipeline/state.mjs +10 -10
  59. package/hub/pipeline/transitions.mjs +40 -23
  60. package/hub/platform.mjs +57 -48
  61. package/hub/promote-penalties.mjs +25 -7
  62. package/hub/quality/deslop.mjs +70 -49
  63. package/hub/research.mjs +32 -25
  64. package/hub/router.mjs +240 -107
  65. package/hub/routing/complexity.mjs +132 -29
  66. package/hub/routing/index.mjs +17 -12
  67. package/hub/routing/q-learning.mjs +76 -28
  68. package/hub/server.mjs +4 -4
  69. package/hub/session-fingerprint.mjs +126 -60
  70. package/hub/state.mjs +84 -43
  71. package/hub/store-adapter.mjs +59 -26
  72. package/hub/store.mjs +356 -153
  73. package/hub/team/agent-map.json +22 -7
  74. package/hub/team/ansi.mjs +186 -122
  75. package/hub/team/backend.mjs +28 -10
  76. package/hub/team/cli/commands/attach.mjs +29 -9
  77. package/hub/team/cli/commands/control.mjs +29 -8
  78. package/hub/team/cli/commands/debug.mjs +32 -11
  79. package/hub/team/cli/commands/focus.mjs +38 -11
  80. package/hub/team/cli/commands/interrupt.mjs +18 -6
  81. package/hub/team/cli/commands/kill.mjs +16 -5
  82. package/hub/team/cli/commands/list.mjs +11 -4
  83. package/hub/team/cli/commands/send.mjs +19 -6
  84. package/hub/team/cli/commands/start/index.mjs +154 -31
  85. package/hub/team/cli/commands/start/parse-args.mjs +38 -11
  86. package/hub/team/cli/commands/start/start-headless.mjs +112 -36
  87. package/hub/team/cli/commands/start/start-in-process.mjs +12 -2
  88. package/hub/team/cli/commands/start/start-mux.mjs +70 -21
  89. package/hub/team/cli/commands/start/start-wt.mjs +29 -12
  90. package/hub/team/cli/commands/status.mjs +43 -14
  91. package/hub/team/cli/commands/stop.mjs +11 -4
  92. package/hub/team/cli/commands/task.mjs +8 -3
  93. package/hub/team/cli/commands/tasks.mjs +1 -1
  94. package/hub/team/cli/index.mjs +2 -2
  95. package/hub/team/cli/manifest.mjs +38 -8
  96. package/hub/team/cli/render.mjs +30 -8
  97. package/hub/team/cli/services/attach-fallback.mjs +31 -11
  98. package/hub/team/cli/services/hub-client.mjs +42 -14
  99. package/hub/team/cli/services/member-selector.mjs +11 -4
  100. package/hub/team/cli/services/native-control.mjs +48 -21
  101. package/hub/team/cli/services/runtime-mode.mjs +2 -1
  102. package/hub/team/cli/services/state-store.mjs +25 -8
  103. package/hub/team/cli/services/task-model.mjs +16 -6
  104. package/hub/team/conductor-mesh-bridge.mjs +24 -23
  105. package/hub/team/conductor.mjs +8 -4
  106. package/hub/team/dashboard-anchor.mjs +4 -5
  107. package/hub/team/dashboard-layout.mjs +3 -1
  108. package/hub/team/dashboard-open.mjs +41 -21
  109. package/hub/team/dashboard.mjs +76 -28
  110. package/hub/team/event-log.mjs +18 -10
  111. package/hub/team/handoff.mjs +31 -15
  112. package/hub/team/headless.mjs +2 -1
  113. package/hub/team/health-probe.mjs +69 -54
  114. package/hub/team/launcher-template.mjs +16 -13
  115. package/hub/team/native-supervisor.mjs +65 -21
  116. package/hub/team/native.mjs +74 -35
  117. package/hub/team/nativeProxy.mjs +184 -113
  118. package/hub/team/notify.mjs +119 -76
  119. package/hub/team/orchestrator.mjs +9 -4
  120. package/hub/team/pane.mjs +12 -7
  121. package/hub/team/process-cleanup.mjs +25 -16
  122. package/hub/team/psmux.mjs +491 -201
  123. package/hub/team/remote-probe.mjs +68 -52
  124. package/hub/team/remote-session.mjs +117 -59
  125. package/hub/team/remote-watcher.mjs +61 -33
  126. package/hub/team/routing.mjs +51 -25
  127. package/hub/team/runtime-strategy.mjs +3 -1
  128. package/hub/team/session.mjs +98 -34
  129. package/hub/team/staleState.mjs +72 -30
  130. package/hub/team/swarm-locks.mjs +15 -13
  131. package/hub/team/swarm-planner.mjs +32 -21
  132. package/hub/team/swarm-reconciler.mjs +48 -23
  133. package/hub/team/tui-lite.mjs +266 -68
  134. package/hub/team/tui-remote-adapter.mjs +14 -10
  135. package/hub/team/tui-viewer.mjs +99 -43
  136. package/hub/team/tui.mjs +708 -271
  137. package/hub/team/worktree-lifecycle.mjs +152 -58
  138. package/hub/team/wt-manager.mjs +24 -14
  139. package/hub/token-mode.mjs +71 -71
  140. package/hub/tray.mjs +66 -23
  141. package/hub/workers/claude-worker.mjs +162 -118
  142. package/hub/workers/codex-mcp.mjs +192 -141
  143. package/hub/workers/delegator-mcp.mjs +507 -333
  144. package/hub/workers/factory.mjs +8 -8
  145. package/hub/workers/gemini-worker.mjs +115 -84
  146. package/hub/workers/interface.mjs +6 -1
  147. package/hub/workers/worker-utils.mjs +21 -14
  148. package/hud/colors.mjs +27 -9
  149. package/hud/constants.mjs +162 -26
  150. package/hud/context-monitor.mjs +82 -41
  151. package/hud/hud-qos-status.mjs +129 -49
  152. package/hud/mission-board.mjs +6 -3
  153. package/hud/providers/claude.mjs +226 -115
  154. package/hud/providers/codex.mjs +62 -22
  155. package/hud/providers/gemini.mjs +168 -56
  156. package/hud/renderers.mjs +384 -119
  157. package/hud/terminal.mjs +101 -31
  158. package/hud/utils.mjs +78 -38
  159. package/mesh/index.mjs +11 -5
  160. package/mesh/mesh-budget.mjs +18 -9
  161. package/mesh/mesh-heartbeat.mjs +1 -1
  162. package/mesh/mesh-queue.mjs +3 -5
  163. package/mesh/mesh-router.mjs +5 -4
  164. package/package.json +2 -1
  165. package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
  166. package/scripts/__tests__/keyword-detector.test.mjs +77 -28
  167. package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
  168. package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
  169. package/scripts/__tests__/remote-spawn.test.mjs +10 -4
  170. package/scripts/__tests__/session-start-fast.test.mjs +36 -0
  171. package/scripts/__tests__/skill-template.test.mjs +98 -50
  172. package/scripts/__tests__/smoke.test.mjs +1 -1
  173. package/scripts/__tests__/spawn-trace.test.mjs +102 -0
  174. package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
  175. package/scripts/cache-doctor.mjs +11 -4
  176. package/scripts/cache-warmup.mjs +96 -37
  177. package/scripts/claudemd-sync.mjs +27 -17
  178. package/scripts/codex-gateway-preflight.mjs +52 -37
  179. package/scripts/codex-mcp-gateway-sync.mjs +59 -39
  180. package/scripts/completions/tfx.bash +47 -47
  181. package/scripts/completions/tfx.fish +44 -44
  182. package/scripts/completions/tfx.zsh +83 -83
  183. package/scripts/config-audit.mjs +232 -0
  184. package/scripts/convert-to-tmpl.mjs +54 -0
  185. package/scripts/cross-review-gate.mjs +35 -12
  186. package/scripts/cross-review-tracker.mjs +21 -8
  187. package/scripts/demo.mjs +35 -17
  188. package/scripts/doctor-diagnose.mjs +284 -0
  189. package/scripts/gen-skill-docs.mjs +7 -2
  190. package/scripts/gen-skill-manifest.mjs +2 -1
  191. package/scripts/headless-guard.mjs +86 -48
  192. package/scripts/hub-ensure.mjs +45 -26
  193. package/scripts/keyword-detector.mjs +41 -20
  194. package/scripts/keyword-rules-expander.mjs +47 -30
  195. package/scripts/lib/claudemd-scanner.mjs +6 -1
  196. package/scripts/lib/context.mjs +3 -3
  197. package/scripts/lib/cross-review-utils.mjs +6 -3
  198. package/scripts/lib/env-probe.mjs +47 -28
  199. package/scripts/lib/gemini-profiles.mjs +44 -10
  200. package/scripts/lib/handoff.mjs +33 -17
  201. package/scripts/lib/hook-utils.mjs +8 -6
  202. package/scripts/lib/keyword-rules.mjs +43 -19
  203. package/scripts/lib/logger.mjs +24 -24
  204. package/scripts/lib/mcp-filter.mjs +377 -239
  205. package/scripts/lib/mcp-guard-engine.mjs +194 -79
  206. package/scripts/lib/mcp-manifest.mjs +23 -13
  207. package/scripts/lib/mcp-server-catalog.mjs +300 -63
  208. package/scripts/lib/psmux-info.mjs +11 -6
  209. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  210. package/scripts/lib/skill-template.mjs +30 -7
  211. package/scripts/mcp-check.mjs +58 -39
  212. package/scripts/mcp-gateway-config.mjs +83 -39
  213. package/scripts/mcp-gateway-ensure.mjs +43 -35
  214. package/scripts/mcp-gateway-integration-test.mjs +70 -58
  215. package/scripts/mcp-gateway-start.mjs +126 -60
  216. package/scripts/mcp-gateway-verify.mjs +24 -22
  217. package/scripts/mcp-safety-guard.mjs +44 -11
  218. package/scripts/notion-read.mjs +199 -84
  219. package/scripts/pack.mjs +94 -89
  220. package/scripts/preflight-cache.mjs +27 -10
  221. package/scripts/preinstall.mjs +42 -13
  222. package/scripts/remote-spawn.mjs +309 -94
  223. package/scripts/run.cjs +8 -5
  224. package/scripts/session-spawn-helper.mjs +130 -39
  225. package/scripts/session-stale-cleanup.mjs +123 -0
  226. package/scripts/setup.mjs +941 -492
  227. package/scripts/test-lock.mjs +20 -7
  228. package/scripts/test-tfx-route-no-claude-native.mjs +16 -12
  229. package/scripts/tfx-batch-stats.mjs +32 -11
  230. package/scripts/tfx-gate-activate.mjs +11 -4
  231. package/scripts/tfx-route-post.mjs +87 -20
  232. package/scripts/tfx-route-worker.mjs +57 -51
  233. package/scripts/tfx-route.sh +41 -124
  234. package/scripts/tmp-cleanup.mjs +21 -7
  235. package/scripts/token-snapshot.mjs +204 -85
  236. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
  237. package/skills/.omc/state/idle-notif-cooldown.json +3 -0
  238. package/skills/.omc/state/last-tool-error.json +7 -0
  239. package/skills/.omc/state/subagent-tracking.json +7 -0
  240. package/skills/_templates/base.md +1 -6
  241. package/skills/merge-worktree/SKILL.md.tmpl +144 -0
  242. package/skills/shared/telemetry-segment.md +6 -0
  243. package/skills/star-prompt/SKILL.md.tmpl +222 -0
  244. package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
  245. package/skills/tfx-analysis/skill.json +1 -6
  246. package/skills/tfx-auto/SKILL.md +1 -0
  247. package/skills/tfx-auto-codex/SKILL.md.tmpl +106 -0
  248. package/skills/tfx-auto-codex/skill.json +1 -3
  249. package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
  250. package/skills/tfx-autopilot/skill.json +1 -5
  251. package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
  252. package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
  253. package/skills/tfx-autoroute/skill.json +1 -7
  254. package/skills/tfx-codex/SKILL.md +1 -0
  255. package/skills/tfx-codex/skill.json +1 -3
  256. package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
  257. package/skills/tfx-codex-swarm/evals/evals.json +1 -1
  258. package/skills/tfx-codex-swarm/skill.json +1 -4
  259. package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
  260. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
  261. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
  262. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
  263. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
  264. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
  265. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
  266. package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
  267. package/skills/tfx-debate/SKILL.md.tmpl +192 -0
  268. package/skills/tfx-debate/skill.json +1 -7
  269. package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
  270. package/skills/tfx-deep-analysis/skill.json +1 -5
  271. package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
  272. package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
  273. package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
  274. package/skills/tfx-deep-qa/skill.json +1 -6
  275. package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
  276. package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
  277. package/skills/tfx-doctor/SKILL.md +21 -0
  278. package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
  279. package/skills/tfx-doctor/skill.json +1 -3
  280. package/skills/tfx-find/SKILL.md +1 -0
  281. package/skills/tfx-forge/SKILL.md.tmpl +187 -0
  282. package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
  283. package/skills/tfx-fullcycle/skill.json +1 -6
  284. package/skills/tfx-gemini/SKILL.md.tmpl +91 -0
  285. package/skills/tfx-gemini/skill.json +1 -3
  286. package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
  287. package/skills/tfx-hooks/skill.json +1 -3
  288. package/skills/tfx-hub/SKILL.md.tmpl +212 -0
  289. package/skills/tfx-hub/skill.json +1 -3
  290. package/skills/tfx-index/SKILL.md +1 -0
  291. package/skills/tfx-index/skill.json +1 -6
  292. package/skills/tfx-interview/SKILL.md.tmpl +285 -0
  293. package/skills/tfx-multi/SKILL.md.tmpl +183 -0
  294. package/skills/tfx-multi/skill.json +1 -3
  295. package/skills/tfx-panel/SKILL.md.tmpl +189 -0
  296. package/skills/tfx-panel/skill.json +1 -7
  297. package/skills/tfx-persist/SKILL.md.tmpl +270 -0
  298. package/skills/tfx-persist/skill.json +1 -7
  299. package/skills/tfx-plan/SKILL.md +1 -0
  300. package/skills/tfx-plan/skill.json +1 -6
  301. package/skills/tfx-profile/SKILL.md.tmpl +239 -0
  302. package/skills/tfx-profile/skill.json +1 -3
  303. package/skills/tfx-prune/SKILL.md.tmpl +200 -0
  304. package/skills/tfx-prune/skill.json +1 -7
  305. package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
  306. package/skills/tfx-psmux-rules/skill.json +1 -4
  307. package/skills/tfx-qa/SKILL.md +1 -0
  308. package/skills/tfx-qa/skill.json +1 -6
  309. package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
  310. package/skills/tfx-ralph/skill.json +1 -4
  311. package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
  312. package/skills/tfx-remote-setup/skill.json +1 -3
  313. package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
  314. package/skills/tfx-remote-spawn/references/hosts.json +16 -0
  315. package/skills/tfx-remote-spawn/skill.json +1 -4
  316. package/skills/tfx-research/SKILL.md +1 -0
  317. package/skills/tfx-review/SKILL.md +1 -0
  318. package/skills/tfx-review/skill.json +1 -6
  319. package/skills/tfx-setup/SKILL.md.tmpl +504 -0
  320. package/skills/tfx-setup/skill.json +1 -3
  321. package/skills/tfx-swarm/SKILL.md +22 -0
  322. package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
  323. package/tui/codex-profile.mjs +88 -33
  324. package/tui/core.mjs +45 -15
  325. package/tui/doctor.mjs +75 -28
  326. package/tui/gemini-profile.mjs +74 -29
  327. package/tui/monitor-data.mjs +8 -4
  328. package/tui/monitor.mjs +71 -27
  329. package/tui/setup.mjs +133 -42
@@ -1,44 +1,77 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  // mcp-safety-guard.mjs — Gemini stdio MCP 자동 감지 + 치환
3
4
  // SessionStart 훅으로 실행. stdio MCP는 Windows에서 spawn EPERM → Gemini stall 유발.
4
5
  // 감지 시 공통 guard engine으로 제거/치환하고 백업 파일 생성.
5
6
 
6
- import { join } from "node:path";
7
7
  import { homedir } from "node:os";
8
- import { loadRegistry, remediate, scanForStdioServers } from "./lib/mcp-guard-engine.mjs";
8
+ import { join } from "node:path";
9
+ import {
10
+ loadRegistry,
11
+ remediate,
12
+ scanForStdioServers,
13
+ } from "./lib/mcp-guard-engine.mjs";
9
14
 
10
15
  const GEMINI_SETTINGS = join(homedir(), ".gemini", "settings.json");
11
16
 
12
- function run() {
17
+ export async function run(stdinData) {
18
+ void stdinData;
19
+
13
20
  let registry;
14
21
  try {
15
22
  registry = loadRegistry();
16
23
  } catch {
17
- return;
24
+ return { code: 0, stdout: "", stderr: "" };
18
25
  }
19
26
 
20
27
  const stdioServers = scanForStdioServers(GEMINI_SETTINGS);
21
28
 
22
- if (stdioServers.length === 0) return; // 모두 안전
29
+ if (stdioServers.length === 0) {
30
+ return { code: 0, stdout: "", stderr: "" };
31
+ }
23
32
 
24
33
  const result = remediate(GEMINI_SETTINGS, stdioServers, registry.policies);
25
34
  const names = stdioServers.map((server) => server.name).join(", ");
35
+ const stdout = [];
26
36
 
27
37
  if (result.modified) {
28
38
  const actionLabel = result.replacement ? "자동 치환" : "자동 제거";
29
- console.log(`[mcp-safety] ${stdioServers.length}개 stdio MCP ${actionLabel}: ${names}`);
39
+ stdout.push(
40
+ `[mcp-safety] ${stdioServers.length}개 stdio MCP ${actionLabel}: ${names}`,
41
+ );
30
42
  if (result.replacement?.name && result.replacement?.url) {
31
- console.log(`[mcp-safety] 대체 서버: ${result.replacement.name} -> ${result.replacement.url}`);
43
+ stdout.push(
44
+ `[mcp-safety] 대체 서버: ${result.replacement.name} -> ${result.replacement.url}`,
45
+ );
32
46
  }
33
47
  if (result.backupPath) {
34
- console.log(`[mcp-safety] 백업: ${result.backupPath}`);
48
+ stdout.push(`[mcp-safety] 백업: ${result.backupPath}`);
35
49
  }
36
- console.log("[mcp-safety] Gemini는 Hub URL만 사용합니다. stdio MCP는 spawn EPERM을 유발합니다.");
50
+ stdout.push(
51
+ "[mcp-safety] Gemini는 Hub URL만 사용합니다. stdio MCP는 spawn EPERM을 유발합니다.",
52
+ );
37
53
  }
38
54
 
39
55
  for (const warning of result.warnings || []) {
40
- console.log(warning);
56
+ stdout.push(warning);
41
57
  }
58
+
59
+ return {
60
+ code: 0,
61
+ stdout: stdout.length > 0 ? `${stdout.join("\n")}\n` : "",
62
+ stderr: "",
63
+ };
42
64
  }
43
65
 
44
- run();
66
+ const isMain =
67
+ process.argv[1] &&
68
+ import.meta.url.endsWith(
69
+ process.argv[1].replace(/\\/g, "/").split("/").pop(),
70
+ );
71
+
72
+ if (isMain) {
73
+ const result = await run();
74
+ if (result.stdout) process.stdout.write(result.stdout);
75
+ if (result.stderr) process.stderr.write(result.stderr);
76
+ process.exit(result.code);
77
+ }
@@ -17,28 +17,35 @@
17
17
  // --guest notion-guest 통합 사용 (기본: notion)
18
18
  // --delegate Claude 이관 모드 (notion-guest 우선, 파일 저장)
19
19
 
20
- import { execSync } from 'child_process';
21
- import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync } from 'fs';
22
- import { join, dirname, resolve } from 'path';
23
- import { homedir, tmpdir } from 'os';
24
-
25
- import { buildExecArgs } from '../hub/codex-adapter.mjs';
26
-
27
- const VERSION = '1.2';
28
- const CLAUDE_DIR = join(homedir(), '.claude');
29
- const MCP_CACHE = join(CLAUDE_DIR, 'cache', 'mcp-inventory.json');
30
- const LOG_FILE = join(CLAUDE_DIR, 'logs', 'tfx-route-stats.jsonl');
31
- const _ACC_FILE = join(CLAUDE_DIR, 'cache', 'sv-accumulator.json');
20
+ import { execSync } from "child_process";
21
+ import {
22
+ appendFileSync,
23
+ existsSync,
24
+ mkdirSync,
25
+ readFileSync,
26
+ unlinkSync,
27
+ writeFileSync,
28
+ } from "fs";
29
+ import { homedir, tmpdir } from "os";
30
+ import { dirname, join, resolve } from "path";
31
+
32
+ import { buildExecArgs } from "../hub/codex-adapter.mjs";
33
+
34
+ const VERSION = "1.2";
35
+ const CLAUDE_DIR = join(homedir(), ".claude");
36
+ const MCP_CACHE = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
37
+ const LOG_FILE = join(CLAUDE_DIR, "logs", "tfx-route-stats.jsonl");
38
+ const _ACC_FILE = join(CLAUDE_DIR, "cache", "sv-accumulator.json");
32
39
 
33
40
  // ── ANSI 색상 ──
34
- const AMBER = '\x1b[38;5;214m';
35
- const GREEN = '\x1b[38;5;82m';
36
- const RED = '\x1b[38;5;196m';
37
- const YELLOW = '\x1b[33m';
38
- const DIM = '\x1b[2m';
39
- const BOLD = '\x1b[1m';
40
- const RESET = '\x1b[0m';
41
- const GRAY = '\x1b[38;5;245m';
41
+ const AMBER = "\x1b[38;5;214m";
42
+ const GREEN = "\x1b[38;5;82m";
43
+ const RED = "\x1b[38;5;196m";
44
+ const YELLOW = "\x1b[33m";
45
+ const DIM = "\x1b[2m";
46
+ const BOLD = "\x1b[1m";
47
+ const RESET = "\x1b[0m";
48
+ const GRAY = "\x1b[38;5;245m";
42
49
 
43
50
  // ── URL 파싱 ──
44
51
  function parseNotionUrl(input) {
@@ -47,7 +54,11 @@ function parseNotionUrl(input) {
47
54
  return { pageId: input, blockId: null };
48
55
  }
49
56
  // UUID 형식
50
- if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(input)) {
57
+ if (
58
+ /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(
59
+ input,
60
+ )
61
+ ) {
51
62
  return { pageId: input.replace(/-/g, ""), blockId: null };
52
63
  }
53
64
  // URL에서 page_id + optional #block_id 추출
@@ -70,12 +81,16 @@ function getNotionMcpClis(useGuest) {
70
81
 
71
82
  if (inv.codex?.servers) {
72
83
  result.codex = inv.codex.servers.some(
73
- (s) => s.name === serverName && (s.status === "enabled" || s.status === "configured"),
84
+ (s) =>
85
+ s.name === serverName &&
86
+ (s.status === "enabled" || s.status === "configured"),
74
87
  );
75
88
  }
76
89
  if (inv.gemini?.servers) {
77
90
  result.gemini = inv.gemini.servers.some(
78
- (s) => s.name === serverName && (s.status === "enabled" || s.status === "configured"),
91
+ (s) =>
92
+ s.name === serverName &&
93
+ (s.status === "enabled" || s.status === "configured"),
79
94
  );
80
95
  }
81
96
  } catch {}
@@ -86,8 +101,16 @@ function getNotionMcpClis(useGuest) {
86
101
  // ── CLI 존재 확인 ──
87
102
  function cliExists(name) {
88
103
  try {
89
- const cmd = process.platform === "win32" ? `where ${name} 2>nul` : `which ${name} 2>/dev/null`;
90
- const result = execSync(cmd, { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "ignore"], windowsHide: true });
104
+ const cmd =
105
+ process.platform === "win32"
106
+ ? `where ${name} 2>nul`
107
+ : `which ${name} 2>/dev/null`;
108
+ const result = execSync(cmd, {
109
+ encoding: "utf8",
110
+ timeout: 5000,
111
+ stdio: ["pipe", "pipe", "ignore"],
112
+ windowsHide: true,
113
+ });
91
114
  return !!result.trim();
92
115
  } catch {
93
116
  return false;
@@ -127,14 +150,18 @@ ${mcpServer} MCP 서버의 도구를 사용하라.
127
150
  각 블록의 has_children이 true이면, 해당 block_id로 블록 자식 조회를 재귀 호출.
128
151
  최대 깊이: ${depth}단계. 깊이 초과 시 "[깊이 초과]" 표시.
129
152
 
130
- ### 4단계: 댓글 수집${includeComments ? `
153
+ ### 4단계: 댓글 수집${
154
+ includeComments
155
+ ? `
131
156
  페이지 및 블록 댓글을 수집하라.
132
157
  - 댓글 조회 도구를 호출하라 (block_id: "${pageId}")로 페이지 전체 댓글을 가져와라.
133
158
  - 응답의 has_more가 true이면 next_cursor로 반복.
134
159
  - 각 댓글의 parent.type이 "block_id"이면 해당 블록의 인라인 댓글이다.
135
160
  - parent.type이 "page_id"이면 페이지 레벨 토론 댓글이다.
136
- - 404 에러 발생 시 댓글 권한이 없는 것이므로 건너뛰어라.` : `
137
- 댓글 수집을 건너뛴다 (--comments 플래그 미지정).`}
161
+ - 404 에러 발생 시 댓글 권한이 없는 것이므로 건너뛰어라.`
162
+ : `
163
+ 댓글 수집을 건너뛴다 (--comments 플래그 미지정).`
164
+ }
138
165
 
139
166
  ### 5단계: 마크다운 변환
140
167
  - heading_1/2/3 → #/##/###
@@ -161,51 +188,63 @@ ${mcpServer} MCP 서버의 도구를 사용하라.
161
188
  - 모든 블록을 빠짐없이 순서대로 출력
162
189
  - 읽기 실패 블록은 <!-- 읽기 실패: block_id --> 주석 남기기
163
190
  - rich_text의 annotations (bold, italic, code, strikethrough) 반영
164
- - 링크는 [텍스트](url) 형식${includeComments ? `
191
+ - 링크는 [텍스트](url) 형식${
192
+ includeComments
193
+ ? `
165
194
  - 블록 인라인 댓글: 해당 블록 바로 아래에 > **[댓글]** @작성자: 내용 형식으로 삽입
166
195
  - 페이지 토론 댓글: 문서 맨 끝에 ## 토론 섹션으로 모아서 출력
167
- - 댓글의 rich_text도 마크다운으로 변환` : ""}
196
+ - 댓글의 rich_text도 마크다운으로 변환`
197
+ : ""
198
+ }
168
199
  - 최종 결과만 출력 — 중간 과정 설명 불필요`;
169
200
  }
170
201
 
171
202
  // ── CLI 실행 (임시 파일 + execSync — Windows .cmd 호환) ──
172
- function runWithCli(cliType, prompt, timeout, runMode = 'fg') {
173
- const cliName = cliType === 'claude' ? 'claude' : cliType === 'codex' ? 'codex' : 'gemini';
203
+ function runWithCli(cliType, prompt, timeout, runMode = "fg") {
204
+ const cliName =
205
+ cliType === "claude" ? "claude" : cliType === "codex" ? "codex" : "gemini";
174
206
  if (!cliExists(cliName)) {
175
- return { success: false, output: '', error: `${cliType} CLI 미설치`, cli: cliType };
207
+ return {
208
+ success: false,
209
+ output: "",
210
+ error: `${cliType} CLI 미설치`,
211
+ cli: cliType,
212
+ };
176
213
  }
177
214
 
178
215
  // 프롬프트를 임시 파일에 저장 (shell escaping 회피)
179
216
  const promptFile = join(tmpdir(), `notion-prompt-${Date.now()}.md`);
180
- writeFileSync(promptFile, prompt, 'utf8');
181
- const promptPath = promptFile.replace(/\\/g, '/');
217
+ writeFileSync(promptFile, prompt, "utf8");
218
+ const promptPath = promptFile.replace(/\\/g, "/");
182
219
 
183
220
  // CLI에 전달할 짧은 메타 프롬프트
184
221
  const metaPrompt = `Read the file at ${promptPath} and execute all instructions in it exactly as described. Output only the final markdown result.`;
185
222
 
186
223
  let cmd;
187
- if (cliType === 'codex') {
224
+ if (cliType === "codex") {
188
225
  cmd = buildExecArgs({ prompt: metaPrompt });
189
- } else if (cliType === 'gemini') {
226
+ } else if (cliType === "gemini") {
190
227
  cmd = `gemini -m gemini-3-flash-preview -y --allowed-mcp-server-names notion,notion-guest --prompt "${metaPrompt}"`;
191
228
  } else {
192
229
  // Claude CLI — print 모드 (MCP 도구 자동 접근)
193
230
  cmd = `claude -p "${metaPrompt}"`;
194
231
  }
195
232
 
196
- console.error(`${AMBER}▸${RESET} ${cliType}로 실행 중... (timeout: ${timeout}s)`);
233
+ console.error(
234
+ `${AMBER}▸${RESET} ${cliType}로 실행 중... (timeout: ${timeout}s)`,
235
+ );
197
236
  const startTime = Date.now();
198
237
 
199
- let stdout = '';
200
- let stderr = '';
238
+ let stdout = "";
239
+ let stderr = "";
201
240
  let exitCode = 0;
202
241
 
203
242
  try {
204
243
  stdout = execSync(cmd, {
205
- encoding: 'utf8',
244
+ encoding: "utf8",
206
245
  timeout: (timeout + 30) * 1000,
207
246
  maxBuffer: 10 * 1024 * 1024,
208
- stdio: ['pipe', 'pipe', 'pipe'],
247
+ stdio: ["pipe", "pipe", "pipe"],
209
248
  cwd: process.cwd(),
210
249
  windowsHide: true,
211
250
  });
@@ -218,7 +257,9 @@ function runWithCli(cliType, prompt, timeout, runMode = 'fg') {
218
257
  const elapsed = Math.round((Date.now() - startTime) / 1000);
219
258
 
220
259
  // 임시 파일 정리
221
- try { unlinkSync(promptFile); } catch {}
260
+ try {
261
+ unlinkSync(promptFile);
262
+ } catch {}
222
263
 
223
264
  // 실행 로그 기록
224
265
  logExecution(cliType, exitCode, elapsed, timeout, stderr, runMode);
@@ -268,30 +309,43 @@ function cleanCodexOutput(raw) {
268
309
  }
269
310
 
270
311
  // ── 실행 로그 (tfx-route.sh 호환) ──
271
- function logExecution(cliType, exitCode, elapsed, timeout, stderr, runMode = 'fg') {
312
+ function logExecution(
313
+ cliType,
314
+ exitCode,
315
+ elapsed,
316
+ timeout,
317
+ stderr,
318
+ runMode = "fg",
319
+ ) {
272
320
  try {
273
321
  const logDir = dirname(LOG_FILE);
274
322
  if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
275
323
 
276
324
  const ts = new Date().toISOString();
277
- const status = exitCode === 0 ? 'success' : exitCode === 124 ? 'timeout' : 'failed';
325
+ const status =
326
+ exitCode === 0 ? "success" : exitCode === 124 ? "timeout" : "failed";
278
327
  const entry = JSON.stringify({
279
328
  ts,
280
- agent: 'notion-read',
329
+ agent: "notion-read",
281
330
  cli: cliType,
282
- effort: cliType === 'codex' ? 'high' : cliType === 'claude' ? 'sonnet' : 'flash',
331
+ effort:
332
+ cliType === "codex"
333
+ ? "high"
334
+ : cliType === "claude"
335
+ ? "sonnet"
336
+ : "flash",
283
337
  run_mode: runMode,
284
- opus_oversight: 'false',
338
+ opus_oversight: "false",
285
339
  status,
286
340
  exit_code: exitCode,
287
341
  elapsed_sec: elapsed,
288
342
  timeout_sec: timeout,
289
- mcp_profile: runMode === 'delegate' ? 'notion-guest' : 'notion',
343
+ mcp_profile: runMode === "delegate" ? "notion-guest" : "notion",
290
344
  input_tokens: 0,
291
345
  output_tokens: 0,
292
346
  total_tokens: 0,
293
347
  });
294
- appendFileSync(LOG_FILE, entry + '\n');
348
+ appendFileSync(LOG_FILE, entry + "\n");
295
349
  } catch {}
296
350
  }
297
351
 
@@ -364,7 +418,7 @@ function main() {
364
418
  case "--comments":
365
419
  includeComments = true;
366
420
  break;
367
- case '--delegate':
421
+ case "--delegate":
368
422
  delegateMode = true;
369
423
  break;
370
424
  }
@@ -380,20 +434,35 @@ function main() {
380
434
  console.error(
381
435
  `${AMBER}▸${RESET} 페이지: ${parsed.pageId}${parsed.blockId ? ` (블록: ${parsed.blockId})` : ""}`,
382
436
  );
383
- console.error(`${GRAY} 통합: ${delegateMode ? 'notion-guest(우선)' : useGuest ? 'notion-guest' : 'notion'} | 깊이: ${depth} | 댓글: ${includeComments ? 'O' : 'X'} | 타임아웃: ${timeout}s${RESET}`);
437
+ console.error(
438
+ `${GRAY} 통합: ${delegateMode ? "notion-guest(우선)" : useGuest ? "notion-guest" : "notion"} | 깊이: ${depth} | 댓글: ${includeComments ? "O" : "X"} | 타임아웃: ${timeout}s${RESET}`,
439
+ );
384
440
 
385
441
  // 프롬프트 생성
386
- const prompt = buildPrompt(parsed.pageId, parsed.blockId, depth, useGuest, includeComments);
442
+ const prompt = buildPrompt(
443
+ parsed.pageId,
444
+ parsed.blockId,
445
+ depth,
446
+ useGuest,
447
+ includeComments,
448
+ );
387
449
  // Claude 폴백용: notion/notion-guest 양쪽 시도 프롬프트
388
- const claudePrompt = buildPrompt(parsed.pageId, parsed.blockId, depth, false, includeComments)
389
- .replace(
390
- 'notion MCP 서버의 도구를 사용하라.',
391
- '가능하면 notion-guest MCP 서버를 먼저 사용하라. 실패하면 notion MCP 서버를 사용하라.',
392
- );
450
+ const claudePrompt = buildPrompt(
451
+ parsed.pageId,
452
+ parsed.blockId,
453
+ depth,
454
+ false,
455
+ includeComments,
456
+ ).replace(
457
+ "notion MCP 서버의 도구를 사용하라.",
458
+ "가능하면 notion-guest MCP 서버를 먼저 사용하라. 실패하면 notion MCP 서버를 사용하라.",
459
+ );
393
460
 
394
461
  // delegate 모드: Claude 단독 + notion-guest 우선 + 파일 저장
395
462
  if (delegateMode) {
396
- console.error(`${AMBER}▸${RESET} delegate 모드 활성화: Claude로 notion-guest 우선 접근`);
463
+ console.error(
464
+ `${AMBER}▸${RESET} delegate 모드 활성화: Claude로 notion-guest 우선 접근`,
465
+ );
397
466
 
398
467
  const delegatePrompt = `${claudePrompt}
399
468
 
@@ -402,34 +471,54 @@ function main() {
402
471
  - notion-guest가 실패하거나 미구성일 때만 notion 서버로 폴백하라.
403
472
  - 도구 호출 결과를 바탕으로 최종 마크다운만 출력하라.`;
404
473
 
405
- const delegateResult = runWithCli('claude', delegatePrompt, timeout, 'delegate');
474
+ const delegateResult = runWithCli(
475
+ "claude",
476
+ delegatePrompt,
477
+ timeout,
478
+ "delegate",
479
+ );
406
480
  if (!delegateResult.success) {
407
- console.error(`${RED}✗${RESET} delegate 모드 실패: ${delegateResult.error}`);
481
+ console.error(
482
+ `${RED}✗${RESET} delegate 모드 실패: ${delegateResult.error}`,
483
+ );
408
484
  if (delegateResult.stderr) {
409
- console.error(`${GRAY} stderr: ${delegateResult.stderr.slice(0, 250)}${RESET}`);
485
+ console.error(
486
+ `${GRAY} stderr: ${delegateResult.stderr.slice(0, 250)}${RESET}`,
487
+ );
410
488
  }
411
- console.error(`${GRAY} 대안: --delegate 없이 실행해 기존 폴백 체인을 사용하세요.${RESET}`);
412
- console.error(`${GRAY} 예: tfx notion-read ${parsed.pageId} --comments${RESET}`);
489
+ console.error(
490
+ `${GRAY} 대안: --delegate 없이 실행해 기존 폴백 체인을 사용하세요.${RESET}`,
491
+ );
492
+ console.error(
493
+ `${GRAY} 예: tfx notion-read ${parsed.pageId} --comments${RESET}`,
494
+ );
413
495
  process.exit(1);
414
496
  }
415
497
 
416
498
  const delegateOutput = delegateResult.output.trim();
417
499
  const isDelegateFailureOutput =
418
- (delegateOutput.includes('조회 실패') || delegateOutput.includes('읽기 실패') || delegateOutput.includes('not_found')) &&
500
+ (delegateOutput.includes("조회 실패") ||
501
+ delegateOutput.includes("읽기 실패") ||
502
+ delegateOutput.includes("not_found")) &&
419
503
  delegateOutput.length < 500;
420
504
 
421
505
  if (delegateOutput.length <= 100 || isDelegateFailureOutput) {
422
- console.error(`${RED}✗${RESET} delegate 모드 실패: Claude 결과가 비정상적입니다.`);
423
- console.error(`${GRAY} 대안: --delegate 없이 실행해 Codex/Gemini/Claude 폴백 체인을 사용하세요.${RESET}`);
506
+ console.error(
507
+ `${RED}✗${RESET} delegate 모드 실패: Claude 결과가 비정상적입니다.`,
508
+ );
509
+ console.error(
510
+ `${GRAY} 대안: --delegate 없이 실행해 Codex/Gemini/Claude 폴백 체인을 사용하세요.${RESET}`,
511
+ );
424
512
  process.exit(1);
425
513
  }
426
514
 
427
- const delegateTarget = outputFile || join('.notion-cache', `${parsed.pageId}.md`);
515
+ const delegateTarget =
516
+ outputFile || join(".notion-cache", `${parsed.pageId}.md`);
428
517
  const delegateDir = dirname(delegateTarget);
429
- if (delegateDir && delegateDir !== '.' && !existsSync(delegateDir)) {
518
+ if (delegateDir && delegateDir !== "." && !existsSync(delegateDir)) {
430
519
  mkdirSync(delegateDir, { recursive: true });
431
520
  }
432
- writeFileSync(delegateTarget, delegateOutput, 'utf8');
521
+ writeFileSync(delegateTarget, delegateOutput, "utf8");
433
522
 
434
523
  const savedPath = resolve(delegateTarget);
435
524
  console.error(`${GREEN}✓${RESET} delegate 결과 저장: ${savedPath}`);
@@ -447,12 +536,18 @@ function main() {
447
536
  if (!mcpAvail.codex && !mcpAvail.gemini) {
448
537
  console.error(`${YELLOW}!${RESET} Codex/Gemini에 Notion MCP 미설치.`);
449
538
  console.error(`${GRAY} Codex: codex mcp add notion${RESET}`);
450
- console.error(`${GRAY} Gemini: ~/.gemini/settings.json에 notion 서버 추가${RESET}`);
539
+ console.error(
540
+ `${GRAY} Gemini: ~/.gemini/settings.json에 notion 서버 추가${RESET}`,
541
+ );
451
542
  console.error(`${GRAY} 설치 후 tfx doctor --reset으로 캐시 갱신${RESET}`);
452
543
  } else if (!mcpAvail.codex) {
453
- console.error(`${GRAY} Codex에 Notion MCP 미설치: codex mcp add notion${RESET}`);
544
+ console.error(
545
+ `${GRAY} Codex에 Notion MCP 미설치: codex mcp add notion${RESET}`,
546
+ );
454
547
  } else if (!mcpAvail.gemini) {
455
- console.error(`${GRAY} Gemini에 Notion MCP 미설치: ~/.gemini/settings.json 확인${RESET}`);
548
+ console.error(
549
+ `${GRAY} Gemini에 Notion MCP 미설치: ~/.gemini/settings.json 확인${RESET}`,
550
+ );
456
551
  }
457
552
 
458
553
  // CLI 실행 순서 결정 (Codex → Gemini → Claude)
@@ -478,16 +573,21 @@ function main() {
478
573
 
479
574
  if (result.success) {
480
575
  // Codex JSON-line 출력 정리
481
- let output = cli === "codex" ? cleanCodexOutput(result.output) : result.output;
576
+ let output =
577
+ cli === "codex" ? cleanCodexOutput(result.output) : result.output;
482
578
  output = output.trim();
483
579
 
484
580
  // 실패 마커 감지 (404, 접근 실패 등)
485
581
  const isFailureOutput =
486
- (output.includes("조회 실패") || output.includes("읽기 실패") || output.includes("not_found")) &&
582
+ (output.includes("조회 실패") ||
583
+ output.includes("읽기 실패") ||
584
+ output.includes("not_found")) &&
487
585
  output.length < 500;
488
586
 
489
587
  if (output.length > 100 && !isFailureOutput) {
490
- console.error(`${GREEN}✓${RESET} ${cli}로 성공 (${output.length} chars, ${result.elapsed}s)`);
588
+ console.error(
589
+ `${GREEN}✓${RESET} ${cli}로 성공 (${output.length} chars, ${result.elapsed}s)`,
590
+ );
491
591
 
492
592
  if (outputFile) {
493
593
  const outDir = dirname(outputFile);
@@ -505,17 +605,25 @@ function main() {
505
605
  // 404 접근 실패 감지
506
606
  if (isFailureOutput) {
507
607
  notionAccessFailed = true;
508
- console.error(`${YELLOW}!${RESET} ${cli}: Notion 접근 실패 (404) — 폴백`);
608
+ console.error(
609
+ `${YELLOW}!${RESET} ${cli}: Notion 접근 실패 (404) — 폴백`,
610
+ );
509
611
  if (!useGuest && cli !== "claude") {
510
- console.error(`${GRAY} --guest 플래그로 notion-guest 통합 시도 가능${RESET}`);
612
+ console.error(
613
+ `${GRAY} --guest 플래그로 notion-guest 통합 시도 가능${RESET}`,
614
+ );
511
615
  }
512
616
  } else {
513
- console.error(`${YELLOW}!${RESET} ${cli}: 출력 부족 (${output.length} chars) — 폴백`);
617
+ console.error(
618
+ `${YELLOW}!${RESET} ${cli}: 출력 부족 (${output.length} chars) — 폴백`,
619
+ );
514
620
  }
515
621
  } else {
516
622
  console.error(`${YELLOW}!${RESET} ${cli} 실패: ${result.error}`);
517
623
  if (result.stderr) {
518
- console.error(`${GRAY} stderr: ${result.stderr.slice(0, 200)}${RESET}`);
624
+ console.error(
625
+ `${GRAY} stderr: ${result.stderr.slice(0, 200)}${RESET}`,
626
+ );
519
627
  }
520
628
  }
521
629
 
@@ -531,16 +639,23 @@ function main() {
531
639
  console.error(`${RED}✗${RESET} 모든 CLI 실패`);
532
640
  if (notionAccessFailed) {
533
641
  console.error(`${YELLOW}!${RESET} Notion 페이지 접근 권한 문제.`);
534
- console.error(`${GRAY} 1. Notion에서 페이지 → ... → 연결(Connections)에서 통합 추가${RESET}`);
535
- console.error(`${GRAY} 2. --guest 플래그로 notion-guest 통합 시도${RESET}`);
536
- console.error(`${GRAY} 3. --cli claude로 Claude 직접 사용 (Claude에 접근 권한 있는 경우)${RESET}`);
642
+ console.error(
643
+ `${GRAY} 1. Notion에서 페이지 ... → 연결(Connections)에서 통합 추가${RESET}`,
644
+ );
645
+ console.error(
646
+ `${GRAY} 2. --guest 플래그로 notion-guest 통합 시도${RESET}`,
647
+ );
648
+ console.error(
649
+ `${GRAY} 3. --cli claude로 Claude 직접 사용 (Claude에 접근 권한 있는 경우)${RESET}`,
650
+ );
537
651
  }
538
652
 
539
653
  // 부분 결과라도 출력
540
654
  if (lastResult?.output) {
541
655
  const partial = lastResult.output.trim();
542
656
  if (partial.length > 10) {
543
- const cleaned = lastResult.cli === "codex" ? cleanCodexOutput(partial) : partial;
657
+ const cleaned =
658
+ lastResult.cli === "codex" ? cleanCodexOutput(partial) : partial;
544
659
  if (outputFile) {
545
660
  writeFileSync(outputFile, cleaned, "utf8");
546
661
  console.error(`${YELLOW}!${RESET} 부분 결과 저장: ${outputFile}`);