triflux 2.4.7 → 3.0.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.
@@ -1,968 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # cli-route.sh v1.6 CLI 라우팅 래퍼 (ai-scaffold 템플릿)
3
- # v1.0: 기본 라우팅 (Codex/Gemini/Claude 분기)
4
- # v1.1: stderr 분리, 출력 필터링, 타임아웃, MCP 프로필 지원
5
- # v1.2: effort 동적 라우팅, bg/fg 모드, Opus 직접 수행, Gemini 모델 분기, 실행 로그
6
- # v1.3: architect/critic Codex 이관, 리뷰어 exec review 전환, multi_agent 활성화
7
- # v1.4: TFX_CLI_MODE 지원 (codex-only/gemini-only), CLI 미설치 자동 fallback
8
- # v1.5: MCP 인벤토리 캐싱 — 실제 서버 가용성 기반 동적 힌트 생성
9
- # v1.6: 토큰 사용량 추출 + sv-accumulator.json 누적
10
- VERSION="1.6"
11
- #
12
- # 설치: cp scripts/cli-route.sh ~/.claude/scripts/cli-route.sh
13
- #
14
- # 사용법:
15
- # cli-route.sh <agent_type> <prompt> [mcp_profile] [timeout_sec]
16
- #
17
- # 예시:
18
- # cli-route.sh executor "코드 구현" implement 300
19
- # cli-route.sh architect "아키텍처 분석" analyze 600
20
- # cli-route.sh designer "UI 리뷰"
21
- # cli-route.sh debugger "버그 분석" analyze
22
-
23
- set -euo pipefail
24
-
25
- # ── 인자 파싱 ──
26
- AGENT_TYPE="${1:?에이전트 타입 필수 (executor, debugger, designer 등)}"
27
- PROMPT="${2:?프롬프트 필수}"
28
- MCP_PROFILE="${3:-auto}"
29
- USER_TIMEOUT="${4:-}"
30
-
31
- # ── CLI 경로 해석 (Windows npm global 대응) ──
32
- CODEX_BIN="${CODEX_BIN:-$(command -v codex 2>/dev/null || echo codex)}"
33
- GEMINI_BIN="${GEMINI_BIN:-$(command -v gemini 2>/dev/null || echo gemini)}"
34
-
35
- # ── 상수 ──
36
- MAX_STDOUT_BYTES=51200 # 50KB — Claude 컨텍스트 절약
37
- TIMESTAMP=$(date +%s)
38
- STDERR_LOG="/tmp/omc-route-${AGENT_TYPE}-${TIMESTAMP}-stderr.log"
39
- STDOUT_LOG="/tmp/omc-route-${AGENT_TYPE}-${TIMESTAMP}-stdout.log"
40
-
41
- # ── 라우팅 테이블 ──
42
- # 반환: CLI_CMD, CLI_ARGS, CLI_TYPE, CLI_EFFORT, DEFAULT_TIMEOUT, RUN_MODE, OPUS_OVERSIGHT
43
- #
44
- # RUN_MODE: bg (백그라운드 — 독립 실행, 결과 나중에 수집)
45
- # fg (포어그라운드 — 결과가 다음 단계를 블로킹)
46
- #
47
- # OPUS_OVERSIGHT: true — Codex 결과를 Claude Opus가 검증/보완해야 함
48
- # false — Codex/Gemini 결과를 그대로 사용
49
- #
50
- route_agent() {
51
- local agent="$1"
52
- local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
53
-
54
- case "$agent" in
55
- # ─── 구현 레인 ───
56
-
57
- # Codex — 코드 구현 (effort: high, 360s, fg — 후속 태스크가 의존)
58
- executor)
59
- CLI_TYPE="codex"
60
- CLI_CMD="codex"
61
- CLI_ARGS="exec ${codex_base}"
62
- CLI_EFFORT="high"
63
- DEFAULT_TIMEOUT=360
64
- RUN_MODE="fg"
65
- OPUS_OVERSIGHT="false"
66
- ;;
67
- # Codex — 빌드 수정 (effort: fast, 180s, fg — 빌드 통과 확인 필요)
68
- build-fixer)
69
- CLI_TYPE="codex"
70
- CLI_CMD="codex"
71
- CLI_ARGS="--profile fast exec ${codex_base}"
72
- CLI_EFFORT="fast"
73
- DEFAULT_TIMEOUT=180
74
- RUN_MODE="fg"
75
- OPUS_OVERSIGHT="false"
76
- ;;
77
- # Codex — 디버깅 (effort: high, 300s, bg — 분석 결과 나중에 수집)
78
- debugger)
79
- CLI_TYPE="codex"
80
- CLI_CMD="codex"
81
- CLI_ARGS="exec ${codex_base}"
82
- CLI_EFFORT="high"
83
- DEFAULT_TIMEOUT=300
84
- RUN_MODE="bg"
85
- OPUS_OVERSIGHT="false"
86
- ;;
87
- # Codex — 자율 실행 (effort: xhigh, 900s, bg — 장시간 독립 수행)
88
- deep-executor)
89
- CLI_TYPE="codex"
90
- CLI_CMD="codex"
91
- CLI_ARGS="--profile xhigh exec ${codex_base}"
92
- CLI_EFFORT="xhigh"
93
- DEFAULT_TIMEOUT=1200
94
- RUN_MODE="bg"
95
- OPUS_OVERSIGHT="true"
96
- ;;
97
-
98
- # ─── 설계/분석 레인 ───
99
-
100
- # Codex — 아키텍처 (effort: xhigh, 900s, bg — Opus가 설계 품질 검증)
101
- architect)
102
- CLI_TYPE="codex"
103
- CLI_CMD="codex"
104
- CLI_ARGS="--profile xhigh exec ${codex_base}"
105
- CLI_EFFORT="xhigh"
106
- DEFAULT_TIMEOUT=1200
107
- RUN_MODE="bg"
108
- OPUS_OVERSIGHT="true"
109
- ;;
110
- # Codex — 태스크 분해 (effort: xhigh, 900s, fg — Opus가 검증)
111
- planner)
112
- CLI_TYPE="codex"
113
- CLI_CMD="codex"
114
- CLI_ARGS="--profile xhigh exec ${codex_base}"
115
- CLI_EFFORT="xhigh"
116
- DEFAULT_TIMEOUT=1200
117
- RUN_MODE="fg"
118
- OPUS_OVERSIGHT="true"
119
- ;;
120
- # Codex — 비판적 검토 (effort: xhigh, 900s, bg — Opus가 비판 품질 검증)
121
- critic)
122
- CLI_TYPE="codex"
123
- CLI_CMD="codex"
124
- CLI_ARGS="--profile xhigh exec ${codex_base}"
125
- CLI_EFFORT="xhigh"
126
- DEFAULT_TIMEOUT=1200
127
- RUN_MODE="bg"
128
- OPUS_OVERSIGHT="true"
129
- ;;
130
- # Codex — 요구사항 분석 (effort: xhigh, 900s, fg — Opus가 검증)
131
- analyst)
132
- CLI_TYPE="codex"
133
- CLI_CMD="codex"
134
- CLI_ARGS="--profile xhigh exec ${codex_base}"
135
- CLI_EFFORT="xhigh"
136
- DEFAULT_TIMEOUT=1200
137
- RUN_MODE="fg"
138
- OPUS_OVERSIGHT="true"
139
- ;;
140
-
141
- # ─── 리뷰 레인 ───
142
-
143
- # Codex — 코드 리뷰 (exec review, effort: thorough, 600s, bg — 전용 리뷰 커맨드)
144
- code-reviewer)
145
- CLI_TYPE="codex"
146
- CLI_CMD="codex"
147
- CLI_ARGS="--profile thorough exec ${codex_base} review"
148
- CLI_EFFORT="thorough"
149
- DEFAULT_TIMEOUT=600
150
- RUN_MODE="bg"
151
- OPUS_OVERSIGHT="false"
152
- ;;
153
- # Codex — 보안 리뷰 (exec review, effort: thorough, 600s, bg — Opus 검증 권장)
154
- security-reviewer)
155
- CLI_TYPE="codex"
156
- CLI_CMD="codex"
157
- CLI_ARGS="--profile thorough exec ${codex_base} review"
158
- CLI_EFFORT="thorough"
159
- DEFAULT_TIMEOUT=600
160
- RUN_MODE="bg"
161
- OPUS_OVERSIGHT="true"
162
- ;;
163
- # Codex — 품질 리뷰 (exec review, effort: thorough, 600s, bg — 전용 리뷰 커맨드)
164
- quality-reviewer)
165
- CLI_TYPE="codex"
166
- CLI_CMD="codex"
167
- CLI_ARGS="--profile thorough exec ${codex_base} review"
168
- CLI_EFFORT="thorough"
169
- DEFAULT_TIMEOUT=600
170
- RUN_MODE="bg"
171
- OPUS_OVERSIGHT="false"
172
- ;;
173
-
174
- # ─── 리서치 레인 ───
175
-
176
- # Codex — 일반 리서치 (effort: high, 480s, bg — 빠른 검색+요약)
177
- scientist)
178
- CLI_TYPE="codex"
179
- CLI_CMD="codex"
180
- CLI_ARGS="exec ${codex_base}"
181
- CLI_EFFORT="high"
182
- DEFAULT_TIMEOUT=480
183
- RUN_MODE="bg"
184
- OPUS_OVERSIGHT="false"
185
- ;;
186
- # Codex — 심층 리서치 (effort: thorough, 1200s, bg — 논문 심층 분석)
187
- scientist-deep)
188
- CLI_TYPE="codex"
189
- CLI_CMD="codex"
190
- CLI_ARGS="--profile thorough exec ${codex_base}"
191
- CLI_EFFORT="thorough"
192
- DEFAULT_TIMEOUT=1200
193
- RUN_MODE="bg"
194
- OPUS_OVERSIGHT="false"
195
- ;;
196
- # Codex — 문서 조사 (effort: high, 480s, bg — 웹 검색 폴백 체인)
197
- document-specialist)
198
- CLI_TYPE="codex"
199
- CLI_CMD="codex"
200
- CLI_ARGS="exec ${codex_base}"
201
- CLI_EFFORT="high"
202
- DEFAULT_TIMEOUT=480
203
- RUN_MODE="bg"
204
- OPUS_OVERSIGHT="false"
205
- ;;
206
-
207
- # ─── UI/문서 레인 ───
208
-
209
- # Gemini Pro 3.1 — UI/디자인 (높은 품질, 시각적 추론)
210
- designer)
211
- CLI_TYPE="gemini"
212
- CLI_CMD="gemini"
213
- CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
214
- CLI_EFFORT="pro"
215
- DEFAULT_TIMEOUT=600
216
- RUN_MODE="bg"
217
- OPUS_OVERSIGHT="false"
218
- ;;
219
- # Gemini Flash 3 — 문서/가이드 작성 (빠른 생성)
220
- writer)
221
- CLI_TYPE="gemini"
222
- CLI_CMD="gemini"
223
- CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
224
- CLI_EFFORT="flash"
225
- DEFAULT_TIMEOUT=600
226
- RUN_MODE="bg"
227
- OPUS_OVERSIGHT="false"
228
- ;;
229
-
230
- # ─── Claude 네이티브 ───
231
-
232
- # Claude Haiku — 코드베이스 탐색 (fg — 탐색 결과가 분해에 필요)
233
- explore)
234
- CLI_TYPE="claude-native"
235
- CLI_CMD=""
236
- CLI_ARGS=""
237
- CLI_EFFORT="n/a"
238
- DEFAULT_TIMEOUT=300
239
- RUN_MODE="fg"
240
- OPUS_OVERSIGHT="false"
241
- ;;
242
- # Claude Sonnet — 검증 (fg — 검증 결과가 다음 단계 결정)
243
- verifier)
244
- CLI_TYPE="claude-native"
245
- CLI_CMD=""
246
- CLI_ARGS=""
247
- CLI_EFFORT="n/a"
248
- DEFAULT_TIMEOUT=300
249
- RUN_MODE="fg"
250
- OPUS_OVERSIGHT="false"
251
- ;;
252
- # Claude Sonnet — 테스트 (bg — 테스트 독립 실행 가능)
253
- test-engineer)
254
- CLI_TYPE="claude-native"
255
- CLI_CMD=""
256
- CLI_ARGS=""
257
- CLI_EFFORT="n/a"
258
- DEFAULT_TIMEOUT=300
259
- RUN_MODE="bg"
260
- OPUS_OVERSIGHT="false"
261
- ;;
262
- # Claude Sonnet — QA (bg — QA 독립 실행 가능)
263
- qa-tester)
264
- CLI_TYPE="claude-native"
265
- CLI_CMD=""
266
- CLI_ARGS=""
267
- CLI_EFFORT="n/a"
268
- DEFAULT_TIMEOUT=300
269
- RUN_MODE="bg"
270
- OPUS_OVERSIGHT="false"
271
- ;;
272
-
273
- # ─── 경량 ───
274
-
275
- # Spark — 린트/보일러플레이트 (120s, fg — 즉시 완료 기대)
276
- spark)
277
- CLI_TYPE="codex"
278
- CLI_CMD="codex"
279
- CLI_ARGS="--profile spark_fast exec ${codex_base}"
280
- CLI_EFFORT="spark_fast"
281
- DEFAULT_TIMEOUT=120
282
- RUN_MODE="fg"
283
- OPUS_OVERSIGHT="false"
284
- ;;
285
- *)
286
- echo "ERROR: 알 수 없는 에이전트 타입: $agent" >&2
287
- echo "사용 가능: executor, build-fixer, debugger, deep-executor, architect, planner, critic, analyst," >&2
288
- echo " code-reviewer, security-reviewer, quality-reviewer, scientist, document-specialist," >&2
289
- echo " designer, writer, explore, verifier, test-engineer, qa-tester, spark" >&2
290
- exit 1
291
- ;;
292
- esac
293
- }
294
-
295
- # ── CLI 모드 오버라이드 (tfx-codex / tfx-gemini 스킬용) ──
296
- # TFX_CLI_MODE: auto (기본), codex (Codex-only), gemini (Gemini-only)
297
- TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
298
-
299
- apply_cli_mode() {
300
- local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
301
-
302
- case "$TFX_CLI_MODE" in
303
- codex)
304
- # Gemini 에이전트를 Codex로 리매핑
305
- if [[ "$CLI_TYPE" == "gemini" ]]; then
306
- CLI_TYPE="codex"
307
- CLI_CMD="codex"
308
- case "$AGENT_TYPE" in
309
- designer)
310
- # UI/디자인은 코드 생성이 필요 → high effort
311
- CLI_ARGS="exec ${codex_base}"
312
- CLI_EFFORT="high"
313
- DEFAULT_TIMEOUT=600
314
- ;;
315
- writer)
316
- # 문서/가이드 작성은 경량 → spark
317
- CLI_ARGS="--profile spark_fast exec ${codex_base}"
318
- CLI_EFFORT="spark_fast"
319
- DEFAULT_TIMEOUT=180
320
- ;;
321
- esac
322
- echo "[cli-route] TFX_CLI_MODE=codex: $AGENT_TYPE → codex($CLI_EFFORT)로 리매핑" >&2
323
- fi
324
- ;;
325
- gemini)
326
- # Codex 에이전트를 Gemini로 리매핑
327
- if [[ "$CLI_TYPE" == "codex" ]]; then
328
- CLI_TYPE="gemini"
329
- CLI_CMD="gemini"
330
- # 복잡한 작업(구현/설계/리뷰/심층분석) → Pro 3.1
331
- # 경량 작업(빌드/린트/검색/문서) → Flash 3
332
- case "$AGENT_TYPE" in
333
- # Pro 3.1 — 깊이 필요한 작업
334
- executor|debugger|deep-executor)
335
- CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
336
- CLI_EFFORT="pro"
337
- ;;
338
- architect|planner|critic|analyst)
339
- CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
340
- CLI_EFFORT="pro"
341
- ;;
342
- code-reviewer|security-reviewer|quality-reviewer)
343
- CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
344
- CLI_EFFORT="pro"
345
- ;;
346
- scientist-deep)
347
- CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
348
- CLI_EFFORT="pro"
349
- ;;
350
- # Flash 3 — 경량/빠른 작업
351
- build-fixer|spark)
352
- CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
353
- CLI_EFFORT="flash"
354
- DEFAULT_TIMEOUT=180
355
- ;;
356
- scientist|document-specialist)
357
- CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
358
- CLI_EFFORT="flash"
359
- ;;
360
- *)
361
- CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
362
- CLI_EFFORT="flash"
363
- ;;
364
- esac
365
- echo "[cli-route] TFX_CLI_MODE=gemini: $AGENT_TYPE → gemini($CLI_EFFORT)로 리매핑" >&2
366
- fi
367
- ;;
368
- auto)
369
- # 자동 감지: CLI 미설치 시 대체
370
- if [[ "$CLI_TYPE" == "codex" ]] && ! command -v "$CODEX_BIN" &>/dev/null; then
371
- if command -v "$GEMINI_BIN" &>/dev/null; then
372
- TFX_CLI_MODE="gemini"
373
- apply_cli_mode
374
- return
375
- else
376
- CLI_TYPE="claude-native"
377
- CLI_CMD=""
378
- CLI_ARGS=""
379
- echo "[cli-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
380
- fi
381
- elif [[ "$CLI_TYPE" == "gemini" ]] && ! command -v "$GEMINI_BIN" &>/dev/null; then
382
- if command -v "$CODEX_BIN" &>/dev/null; then
383
- TFX_CLI_MODE="codex"
384
- apply_cli_mode
385
- return
386
- else
387
- CLI_TYPE="claude-native"
388
- CLI_CMD=""
389
- CLI_ARGS=""
390
- echo "[cli-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
391
- fi
392
- fi
393
- ;;
394
- esac
395
- }
396
-
397
- # ── MCP 인벤토리 캐시 읽기 ──
398
- MCP_CACHE="${HOME}/.claude/cache/mcp-inventory.json"
399
-
400
- # 캐시에서 특정 CLI의 서버 목록 추출 (캐시 없으면 빈 문자열)
401
- get_cached_servers() {
402
- local cli_type="$1"
403
- if [[ -f "$MCP_CACHE" ]]; then
404
- # node로 JSON 파싱 — 인자 전달 방식 (Windows 호환)
405
- node -e 'const[,f,t]=process.argv;const inv=JSON.parse(require("fs").readFileSync(f,"utf8"));const s=(inv[t]||{}).servers||[];console.log(s.filter(x=>x.status==="enabled"||x.status==="configured").map(x=>x.name).join(","))' -- "$MCP_CACHE" "$cli_type" 2>/dev/null
406
- fi
407
- }
408
-
409
- # ── MCP 프로필 → 프롬프트 접미사 ──
410
- get_mcp_hint() {
411
- local profile="$1"
412
- local agent="$2"
413
-
414
- # auto 모드: 에이전트에 따라 자동 결정
415
- if [[ "$profile" == "auto" ]]; then
416
- case "$agent" in
417
- executor|build-fixer|debugger)
418
- profile="implement"
419
- ;;
420
- architect|planner|critic|analyst)
421
- profile="analyze"
422
- ;;
423
- code-reviewer|security-reviewer|quality-reviewer)
424
- profile="review"
425
- ;;
426
- scientist|document-specialist)
427
- profile="analyze"
428
- ;;
429
- deep-executor)
430
- profile="implement"
431
- ;;
432
- designer|writer)
433
- profile="docs"
434
- ;;
435
- *)
436
- profile="minimal"
437
- ;;
438
- esac
439
- fi
440
-
441
- # 동적 힌트: 캐시가 있으면 실제 서버 목록 기반으로 생성
442
- local cached_servers=""
443
- cached_servers=$(get_cached_servers "$CLI_TYPE")
444
-
445
- # 서버 존재 여부 헬퍼
446
- has_server() { echo ",$cached_servers," | grep -q ",$1,"; }
447
-
448
- # 캐시가 있으면 동적 힌트, 없으면 기본 힌트
449
- if [[ -n "$cached_servers" ]]; then
450
- local hint=""
451
- case "$profile" in
452
- implement)
453
- has_server "context7" && hint="${hint}context7으로 라이브러리 문서를 조회하세요. "
454
- if has_server "brave-search"; then
455
- hint="${hint}웹 검색은 brave-search를 사용하세요. "
456
- elif has_server "exa"; then
457
- hint="${hint}웹 검색은 exa를 사용하세요. "
458
- elif has_server "tavily"; then
459
- hint="${hint}웹 검색은 tavily를 사용하세요. "
460
- fi
461
- hint="${hint}검색 도구 실패 시 재시도하지 말고 다음 도구로 전환하세요."
462
- echo "$hint"
463
- ;;
464
- analyze)
465
- has_server "context7" && hint="${hint}context7으로 관련 문서를 조회하세요. "
466
- # 검색 도구 우선순위 동적 구성
467
- local search_tools=""
468
- has_server "brave-search" && search_tools="${search_tools}brave-search, "
469
- has_server "tavily" && search_tools="${search_tools}tavily, "
470
- has_server "exa" && search_tools="${search_tools}exa, "
471
- if [[ -n "$search_tools" ]]; then
472
- hint="${hint}웹 검색 우선순위: ${search_tools%%, }. 402 에러 시 즉시 다음 도구로 전환. "
473
- fi
474
- has_server "playwright" && hint="${hint}모든 검색 실패 시 playwright로 직접 방문 (최대 3 URL). "
475
- hint="${hint}검색 깊이를 제한하고 결과를 빠르게 요약하세요."
476
- echo "$hint"
477
- ;;
478
- review)
479
- has_server "sequential-thinking" && echo "sequential-thinking으로 체계적으로 분석하세요." || echo ""
480
- ;;
481
- docs)
482
- has_server "context7" && hint="${hint}context7으로 공식 문서를 참조하세요. "
483
- has_server "brave-search" && hint="${hint}추가 검색은 brave-search를 사용하세요. "
484
- hint="${hint}검색 결과의 출처 URL을 함께 제시하세요."
485
- echo "$hint"
486
- ;;
487
- minimal|none) echo "" ;;
488
- *) echo "" ;;
489
- esac
490
- else
491
- # 캐시 없음 → 기본 힌트 (기존 동작 유지)
492
- case "$profile" in
493
- implement)
494
- echo "context7으로 라이브러리 문서를 조회하세요. 웹 검색이 필요하면 brave-search를 우선 사용하고, 실패 시 exa를 시도하세요. exa/tavily가 402 에러를 반환하면 즉시 brave-search로 전환하세요."
495
- ;;
496
- analyze)
497
- echo "context7으로 관련 문서를 조회하세요. 웹 검색은 다음 우선순위로 사용: 1) brave-search (무료, 우선), 2) tavily (실패 시), 3) exa (최후 수단). 402 에러 발생 시 해당 도구를 재시도하지 말고 즉시 다음 도구로 전환하세요. 모든 검색 도구가 실패하면 playwright로 직접 웹페이지를 방문하여 정보를 수집하세요. 주의: URL 크롤링은 최대 3개까지만. 논문 전문 대신 제목/초록/핵심 수치만 정리하세요. 검색 깊이를 제한하고 결과를 빠르게 요약하세요."
498
- ;;
499
- review)
500
- echo "sequential-thinking으로 체계적으로 분석하세요."
501
- ;;
502
- docs)
503
- echo "context7으로 공식 문서를 참조하세요. 추가 검색이 필요하면 brave-search를 사용하세요. 사실 확인이 필요한 내용은 반드시 Google Search를 활용하여 검증하세요 (google_search 도구 사용). 검색 결과의 출처 URL을 함께 제시하세요."
504
- ;;
505
- minimal|none) echo "" ;;
506
- *) echo "" ;;
507
- esac
508
- fi
509
- }
510
-
511
- # ── Gemini MCP 서버 선택적 로드 (병렬 경합 감소) ──
512
- # MCP 프로필별 필요한 서버만 로드하여 초기화 시간 단축
513
- get_gemini_mcp_filter() {
514
- local profile="$1"
515
- case "$profile" in
516
- implement) echo "--allowed-mcp-server-names context7,brave-search" ;;
517
- analyze) echo "--allowed-mcp-server-names context7,brave-search,exa" ;;
518
- review) echo "--allowed-mcp-server-names sequential-thinking" ;;
519
- docs) echo "--allowed-mcp-server-names context7,brave-search" ;;
520
- *) echo "" ;; # 필터 없음 — 모든 서버 로드
521
- esac
522
- }
523
-
524
- # ── 토큰 사용량 추출 ──
525
- # Codex JSON-line에서 usage 필드를 파싱하여 "input output" 반환
526
- # Gemini는 세션 파일에서 추출하므로 여기선 0 반환
527
- extract_tokens() {
528
- local raw="$1"
529
- local cli_type="$2"
530
- local stderr_file="$3"
531
-
532
- if [[ "$cli_type" == "codex" ]]; then
533
- # Codex CLI: stderr에 "tokens used\n76,239" 형식으로 토큰 출력
534
- if [[ -f "$stderr_file" ]]; then
535
- local total
536
- total=$(grep -A1 "tokens used" "$stderr_file" 2>/dev/null | tail -1 | tr -d ',' | tr -d ' ')
537
- if [[ -n "$total" && "$total" =~ ^[0-9]+$ && "$total" -gt 0 ]]; then
538
- echo "$total 0"
539
- return
540
- fi
541
- fi
542
- echo "0 0"
543
- return
544
- fi
545
-
546
- if [[ "$cli_type" == "gemini" ]]; then
547
- # Gemini CLI: ~/.gemini/tmp/*/chats/session-*.json에서 최신 세션 토큰 추출
548
- local gemini_tmp="${HOME}/.gemini/tmp"
549
- if [[ -d "$gemini_tmp" ]]; then
550
- local latest
551
- latest=$(find "$gemini_tmp" -name "session-*.json" -path "*/chats/*" -newer "$stderr_file" 2>/dev/null \
552
- | head -1)
553
- # stderr보다 새 파일 없으면 가장 최근 파일 사용
554
- if [[ -z "$latest" ]]; then
555
- latest=$(find "$gemini_tmp" -name "session-*.json" -path "*/chats/*" -printf '%T@ %p\n' 2>/dev/null \
556
- | sort -rn | head -1 | cut -d' ' -f2-)
557
- # Windows Git Bash: -printf 미지원 시 ls fallback
558
- if [[ -z "$latest" ]]; then
559
- latest=$(find "$gemini_tmp" -name "session-*.json" -path "*/chats/*" 2>/dev/null \
560
- | xargs ls -t 2>/dev/null | head -1)
561
- fi
562
- fi
563
- if [[ -n "$latest" && -f "$latest" ]]; then
564
- local result
565
- result=$(python3 -c "
566
- import json, sys
567
- data = json.load(open(sys.argv[1]))
568
- inp = sum(m.get('tokens',{}).get('input',0) for m in data.get('messages',[]))
569
- out = sum(m.get('tokens',{}).get('output',0) for m in data.get('messages',[]))
570
- print(f'{inp} {out}')
571
- " "$latest" 2>/dev/null) || result="0 0"
572
- local inp out
573
- inp=$(echo "$result" | awk '{print $1}')
574
- out=$(echo "$result" | awk '{print $2}')
575
- if [[ $((inp + out)) -gt 0 ]]; then
576
- echo "$inp $out"
577
- return
578
- fi
579
- fi
580
- fi
581
- echo "0 0"
582
- return
583
- fi
584
-
585
- echo "0 0"
586
- }
587
-
588
- # ── Codex JSON-line 출력 파서 ──
589
- filter_codex_output() {
590
- local raw="$1"
591
-
592
- # JSON-line 형식이면 파싱, 아니면 그대로 반환
593
- if echo "$raw" | head -1 | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then
594
- echo "$raw" | python3 -c "
595
- import sys, json
596
- for line in sys.stdin:
597
- line = line.strip()
598
- if not line:
599
- continue
600
- try:
601
- obj = json.loads(line)
602
- if obj.get('type') in ('message', 'completed', 'output_text'):
603
- text = obj.get('text', obj.get('content', obj.get('output', '')))
604
- if text:
605
- print(text)
606
- except json.JSONDecodeError:
607
- print(line)
608
- " 2>/dev/null || echo "$raw"
609
- else
610
- echo "$raw"
611
- fi
612
- }
613
-
614
- # ── 실행 로그 기록 ──
615
- # 각 실행의 에이전트, effort, 소요시간, 상태를 로컬에 누적 기록
616
- LOG_DIR="${HOME}/.claude/logs"
617
- LOG_FILE="${LOG_DIR}/cli-route-stats.jsonl"
618
-
619
- log_execution() {
620
- local agent="$1"
621
- local cli_type="$2"
622
- local effort="$3"
623
- local run_mode="$4"
624
- local opus="$5"
625
- local exit_code="$6"
626
- local elapsed="$7"
627
- local timeout="$8"
628
- local mcp_profile="$9"
629
- local input_tokens="${10:-0}"
630
- local output_tokens="${11:-0}"
631
- local total_tokens="${12:-0}"
632
-
633
- mkdir -p "$LOG_DIR"
634
-
635
- local ts
636
- ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%S")
637
- local status="success"
638
- if [[ $exit_code -eq 124 ]]; then
639
- status="timeout"
640
- elif [[ $exit_code -ne 0 ]]; then
641
- status="failed"
642
- fi
643
-
644
- # JSONL 한 줄 추가 (jq 없이 수동 구성)
645
- printf '{"ts":"%s","agent":"%s","cli":"%s","effort":"%s","run_mode":"%s","opus_oversight":"%s","status":"%s","exit_code":%d,"elapsed_sec":%d,"timeout_sec":%d,"mcp_profile":"%s","input_tokens":%d,"output_tokens":%d,"total_tokens":%d}\n' \
646
- "$ts" "$agent" "$cli_type" "$effort" "$run_mode" "$opus" "$status" "$exit_code" "$elapsed" "$timeout" "$mcp_profile" \
647
- "$input_tokens" "$output_tokens" "$total_tokens" \
648
- >> "$LOG_FILE" 2>/dev/null || true
649
- }
650
-
651
- # ── 토큰 누적 (sv-accumulator.json) ──
652
- # 실행 성공 시 추출된 토큰을 ~/.claude/cache/sv-accumulator.json에 누적
653
- accumulate_tokens() {
654
- local cli_type="$1"
655
- local input_tokens="$2"
656
- local output_tokens="$3"
657
- local total=$((input_tokens + output_tokens))
658
-
659
- # 토큰 0이면 건너뜀
660
- if [[ $total -eq 0 ]]; then return; fi
661
-
662
- local acc_file="${HOME}/.claude/cache/sv-accumulator.json"
663
- mkdir -p "$(dirname "$acc_file")"
664
-
665
- # node로 JSON 읽기/수정/쓰기 (jq 의존성 없이)
666
- node -e '
667
- const fs = require("fs");
668
- const [, file, cliType, inp, out] = process.argv;
669
- let data;
670
- try { data = JSON.parse(fs.readFileSync(file, "utf-8")); } catch { data = {}; }
671
- if (!data.codex) data.codex = { tokens: 0, calls: 0 };
672
- if (!data.gemini) data.gemini = { tokens: 0, calls: 0 };
673
- const key = cliType === "gemini" ? "gemini" : "codex";
674
- data[key].tokens += Number(inp) + Number(out);
675
- data[key].calls += 1;
676
- data.lastUpdated = new Date().toISOString();
677
- fs.writeFileSync(file, JSON.stringify(data, null, 2));
678
- ' -- "$acc_file" "$cli_type" "$input_tokens" "$output_tokens" 2>/dev/null || true
679
- }
680
-
681
- # ── CLI 이슈 자동 수집 ──
682
- # stderr에서 알려진 에러 패턴을 감지하여 ~/.claude/cache/cli-issues.jsonl에 기록
683
- track_cli_issue() {
684
- local cli_type="$1" agent="$2" stderr_text="$3" exit_code="$4"
685
- [[ -z "$stderr_text" && "$exit_code" -eq 0 ]] && return
686
-
687
- local issues_file="${HOME}/.claude/cache/cli-issues.jsonl"
688
- mkdir -p "$(dirname "$issues_file")"
689
-
690
- local pattern="" msg="" severity="warn"
691
-
692
- # 패턴 매칭 (가장 구체적인 것 우선)
693
- if echo "$stderr_text" | grep -qi "sandbox image.*missing"; then
694
- pattern="sandbox_missing"; msg="Docker sandbox image not found"; severity="warn"
695
- elif echo "$stderr_text" | grep -qi "rate.limit\|429\|too many requests"; then
696
- pattern="rate_limit"; msg="API rate limit exceeded"; severity="warn"
697
- elif echo "$stderr_text" | grep -qi "ECONNREFUSED\|ENOTFOUND\|network"; then
698
- pattern="network_error"; msg="Network connection failed"; severity="error"
699
- elif echo "$stderr_text" | grep -qi "deprecated"; then
700
- pattern="deprecated_flag"; msg="Deprecated flag/feature detected"; severity="warn"
701
- elif echo "$stderr_text" | grep -qi "API_KEY.*not.set\|auth.*fail\|unauthorized\|401"; then
702
- pattern="auth_error"; msg="Authentication failed"; severity="error"
703
- elif echo "$stderr_text" | grep -qi "ENOMEM\|out of memory\|heap"; then
704
- pattern="oom"; msg="Out of memory"; severity="error"
705
- elif [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
706
- pattern="unknown_error"; msg="Exit code $exit_code"; severity="warn"
707
- fi
708
-
709
- [[ -z "$pattern" ]] && return
710
-
711
- # 중복 방지: 같은 패턴+cli가 최근 5분 내 기록됐으면 건너뜀
712
- if [[ -f "$issues_file" ]]; then
713
- local now_ms=$(($(date +%s) * 1000))
714
- local dedup
715
- dedup=$(tail -5 "$issues_file" 2>/dev/null | grep "\"$pattern\"" | grep "\"$cli_type\"" | tail -1)
716
- if [[ -n "$dedup" ]]; then
717
- local last_ts
718
- last_ts=$(echo "$dedup" | sed 's/.*"ts":\([0-9]*\).*/\1/' 2>/dev/null)
719
- if [[ -n "$last_ts" ]] && (( now_ms - last_ts < 300000 )); then
720
- return # 5분 이내 동일 이슈 → 건너뜀
721
- fi
722
- fi
723
- fi
724
-
725
- # stderr 첫 200자만 기록 (개인정보 최소화)
726
- local snippet
727
- snippet=$(echo "$stderr_text" | head -3 | cut -c1-200 | tr '\n' ' ')
728
-
729
- # CLI 버전 추출
730
- local cli_ver=""
731
- cli_ver=$($cli_type --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
732
-
733
- node -e '
734
- const fs = require("fs");
735
- const [,, file, cli, agent, pattern, msg, severity, snippet, ver] = process.argv;
736
- const entry = JSON.stringify({
737
- ts: Date.now(), cli, agent, pattern, msg, severity,
738
- snippet: snippet.substring(0, 200), ver: ver || null, resolved: false
739
- });
740
- fs.appendFileSync(file, entry + "\n");
741
- // 자동 회전: 200줄 초과 시 최근 100줄만 유지
742
- const lines = fs.readFileSync(file, "utf-8").trim().split("\n");
743
- if (lines.length > 200) {
744
- fs.writeFileSync(file, lines.slice(-100).join("\n") + "\n");
745
- }
746
- ' -- "$issues_file" "$cli_type" "$agent" "$pattern" "$msg" "$severity" "$snippet" "$cli_ver" 2>/dev/null || true
747
- }
748
-
749
- # ── 출력 크기 제한 ──
750
- truncate_output() {
751
- local input="$1"
752
- local max_bytes="$2"
753
- local byte_count
754
- byte_count=$(echo "$input" | wc -c)
755
-
756
- if [[ $byte_count -gt $max_bytes ]]; then
757
- echo "$input" | head -c "$max_bytes"
758
- echo ""
759
- echo "--- [출력 ${byte_count}B → ${max_bytes}B로 절삭됨] ---"
760
- else
761
- echo "$input"
762
- fi
763
- }
764
-
765
- # ── 메인 실행 ──
766
- main() {
767
- route_agent "$AGENT_TYPE"
768
-
769
- # CLI 모드 오버라이드 적용 (tfx-codex/tfx-gemini 또는 auto-fallback)
770
- apply_cli_mode
771
-
772
- # CLI 경로 해석 (bare command → 절대경로)
773
- case "$CLI_CMD" in
774
- codex) CLI_CMD="$CODEX_BIN" ;;
775
- gemini) CLI_CMD="$GEMINI_BIN" ;;
776
- esac
777
-
778
- # 사용자 지정 타임아웃이 없으면 에이전트별 기본값 사용
779
- if [[ -n "$USER_TIMEOUT" ]]; then
780
- TIMEOUT_SEC="$USER_TIMEOUT"
781
- else
782
- TIMEOUT_SEC="$DEFAULT_TIMEOUT"
783
- fi
784
-
785
- # kteam 안정화: Gemini 에이전트 기본 타임아웃 축소 (사용자 미지정 시만)
786
- if [[ -z "$USER_TIMEOUT" ]]; then
787
- case "$AGENT_TYPE" in
788
- designer|writer)
789
- if [[ "$DEFAULT_TIMEOUT" -gt 60 ]]; then
790
- TIMEOUT_SEC=60
791
- fi
792
- ;;
793
- esac
794
- fi
795
-
796
- # Claude 네이티브 에이전트는 이 스크립트로 처리 불가
797
- if [[ "$CLI_TYPE" == "claude-native" ]]; then
798
- # 에이전트별 모델 결정
799
- local model="sonnet"
800
- case "$AGENT_TYPE" in
801
- explore) model="haiku" ;;
802
- verifier|test-engineer|qa-tester) model="sonnet" ;;
803
- esac
804
-
805
- echo "ROUTE_TYPE=claude-native"
806
- echo "AGENT=$AGENT_TYPE"
807
- echo "MODEL=$model"
808
- echo "RUN_MODE=$RUN_MODE"
809
- echo "OPUS_OVERSIGHT=$OPUS_OVERSIGHT"
810
- echo "TIMEOUT=$TIMEOUT_SEC"
811
- echo "PROMPT=$PROMPT"
812
- echo "--- Claude Task($model) 에이전트로 위임하세요 ---"
813
- exit 0
814
- fi
815
-
816
- # MCP 힌트를 프롬프트에 주입
817
- MCP_HINT=$(get_mcp_hint "$MCP_PROFILE" "$AGENT_TYPE")
818
- if [[ -n "$MCP_HINT" ]]; then
819
- FULL_PROMPT="${PROMPT}. ${MCP_HINT}"
820
- else
821
- FULL_PROMPT="$PROMPT"
822
- fi
823
-
824
- # 메타정보 출력 (stderr로)
825
- echo "[cli-route] type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
826
- echo "[cli-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE stderr_log=$STDERR_LOG" >&2
827
-
828
- # CLI 실행 (stderr 분리 + 타임아웃 + 소요시간 측정)
829
- local exit_code=0
830
- local raw_output=""
831
- local start_time
832
- start_time=$(date +%s)
833
-
834
- if [[ "$CLI_TYPE" == "codex" ]]; then
835
- raw_output=$(timeout "$TIMEOUT_SEC" $CLI_CMD $CLI_ARGS "$FULL_PROMPT" 2>"$STDERR_LOG") || exit_code=$?
836
- elif [[ "$CLI_TYPE" == "gemini" ]]; then
837
- # Gemini 안정화 v2: 프로세스 생존 기반 health check + MCP 선택적 로드
838
- # - 프로세스 살아있음 → 정상 (MCP 초기화 중일 수 있음)
839
- # - 프로세스 죽음 + 출력 없음 → crash → 재시도
840
- # - 메인 timeout이 진짜 hang을 처리
841
-
842
- # Fix 4: MCP 프로필별 필요한 서버만 로드 (병렬 경합 감소)
843
- local gemini_mcp_filter
844
- gemini_mcp_filter=$(get_gemini_mcp_filter "$MCP_PROFILE")
845
- local gemini_args="$CLI_ARGS"
846
- if [[ -n "$gemini_mcp_filter" ]]; then
847
- gemini_args="${CLI_ARGS/--prompt/$gemini_mcp_filter --prompt}"
848
- echo "[cli-route] Gemini MCP 필터: $gemini_mcp_filter" >&2
849
- fi
850
-
851
- timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
852
- local pid=$!
853
-
854
- # Fix 2+3: 프로세스 생존 기반 health check (30초)
855
- local health_ok=true
856
- local HEALTH_TIMEOUT=30
857
- for i in $(seq 1 $HEALTH_TIMEOUT); do
858
- sleep 1
859
- # 출력 있으면 확실히 정상 → 조기 탈출
860
- if [[ -s "$STDOUT_LOG" ]] || [[ -s "$STDERR_LOG" ]]; then
861
- break
862
- fi
863
- # 프로세스 사망 + 출력 없음 → crash
864
- if ! kill -0 "$pid" 2>/dev/null; then
865
- health_ok=false
866
- echo "[cli-route] Gemini: 출력 없이 프로세스 종료 (${i}초)" >&2
867
- break
868
- fi
869
- # 프로세스 살아있고 출력 없음 → MCP 초기화 중, 계속 대기
870
- done
871
-
872
- if [[ "$health_ok" == "false" ]]; then
873
- # crash 감지 → 1회 재시도
874
- wait "$pid" 2>/dev/null
875
- echo "[cli-route] Gemini crash 감지, 재시도 중..." >&2
876
- timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
877
- pid=$!
878
- wait "$pid"
879
- exit_code=$?
880
- else
881
- wait "$pid"
882
- exit_code=$?
883
- fi
884
-
885
- raw_output=$(cat "$STDOUT_LOG" 2>/dev/null)
886
- fi
887
-
888
- local end_time
889
- end_time=$(date +%s)
890
- local elapsed=$((end_time - start_time))
891
-
892
- # 토큰 추출
893
- local token_info input_tokens output_tokens total_tokens
894
- token_info=$(extract_tokens "$raw_output" "$CLI_TYPE" "$STDERR_LOG") || token_info="0 0"
895
- input_tokens=$(echo "$token_info" | awk '{print $1}')
896
- output_tokens=$(echo "$token_info" | awk '{print $2}')
897
- total_tokens=$((input_tokens + output_tokens))
898
-
899
- # 실행 로그 기록 (토큰 포함)
900
- log_execution "$AGENT_TYPE" "$CLI_TYPE" "$CLI_EFFORT" "$RUN_MODE" "$OPUS_OVERSIGHT" \
901
- "$exit_code" "$elapsed" "$TIMEOUT_SEC" "$MCP_PROFILE" \
902
- "$input_tokens" "$output_tokens" "$total_tokens"
903
-
904
- # 성공 시 토큰 누적
905
- if [[ $exit_code -eq 0 ]]; then
906
- accumulate_tokens "$CLI_TYPE" "$input_tokens" "$output_tokens" || true
907
- fi
908
-
909
- # CLI 이슈 자동 수집
910
- local _stderr_for_track=""
911
- [[ -f "$STDERR_LOG" ]] && _stderr_for_track=$(cat "$STDERR_LOG" 2>/dev/null || echo "")
912
- track_cli_issue "$CLI_TYPE" "$AGENT_TYPE" "$_stderr_for_track" "$exit_code" || true
913
-
914
- # 결과 처리
915
- local stderr_content=""
916
- if [[ -f "$STDERR_LOG" ]]; then
917
- stderr_content=$(cat "$STDERR_LOG" 2>/dev/null || echo "")
918
- fi
919
-
920
- # 헤더 (구조화된 메타데이터)
921
- echo "=== CLI-ROUTE RESULT ==="
922
- echo "agent: $AGENT_TYPE"
923
- echo "cli: $CLI_TYPE ($CLI_CMD)"
924
- echo "effort: $CLI_EFFORT"
925
- echo "run_mode: $RUN_MODE"
926
- echo "opus_oversight: $OPUS_OVERSIGHT"
927
- echo "exit_code: $exit_code"
928
- echo "timeout: ${TIMEOUT_SEC}s"
929
- echo "elapsed: ${elapsed}s"
930
- echo "mcp_profile: $MCP_PROFILE"
931
- echo "stderr_log: $STDERR_LOG"
932
-
933
- # exit code 분석
934
- if [[ $exit_code -eq 0 ]]; then
935
- if [[ -n "$stderr_content" ]]; then
936
- echo "status: success_with_warnings"
937
- echo "warnings: $(echo "$stderr_content" | head -3)"
938
- else
939
- echo "status: success"
940
- fi
941
- echo "=== OUTPUT ==="
942
-
943
- if [[ "$CLI_TYPE" == "codex" ]]; then
944
- filtered=$(filter_codex_output "$raw_output")
945
- truncate_output "$filtered" "$MAX_STDOUT_BYTES"
946
- else
947
- truncate_output "$raw_output" "$MAX_STDOUT_BYTES"
948
- fi
949
-
950
- elif [[ $exit_code -eq 124 ]]; then
951
- echo "status: timeout (${TIMEOUT_SEC}s 초과)"
952
- echo "=== PARTIAL OUTPUT ==="
953
- truncate_output "$raw_output" "$MAX_STDOUT_BYTES"
954
- echo "=== STDERR ==="
955
- echo "$stderr_content" | tail -10
956
-
957
- else
958
- echo "status: failed (exit_code=$exit_code)"
959
- echo "=== STDERR ==="
960
- echo "$stderr_content" | tail -20
961
- if [[ -n "$raw_output" ]]; then
962
- echo "=== PARTIAL OUTPUT ==="
963
- truncate_output "$raw_output" "$MAX_STDOUT_BYTES"
964
- fi
965
- fi
966
- }
967
-
968
- main
2
+ # cli-route.sh — backward-compat 래퍼
3
+ exec bash "$(dirname "$0")/tfx-route.sh" "$@"