triflux 10.52.0 → 10.53.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.
- package/CLAUDE.md +0 -16
- package/README.ko.md +2 -1
- package/README.md +3 -2
- package/adapters/codex/skills/tfx-live/SKILL.md +15 -3
- package/bin/tfx-live.mjs +185 -15
- package/bin/triflux.mjs +14 -15
- package/config/mcp-registry.json +5 -6
- package/hub/bridge.mjs +4 -25
- package/hub/cli-adapter-base.mjs +4 -104
- package/hub/codex-adapter.mjs +7 -116
- package/hub/team/claude-transcript.mjs +19 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -0
- package/hub/team/codex-queue.mjs +108 -1
- package/hub/team/headless.mjs +36 -8
- package/hub/team/results-index.mjs +92 -0
- package/hub/team/uds-orchestrator.mjs +18 -19
- package/hud/constants.mjs +7 -22
- package/hud/hud-qos-status.mjs +9 -28
- package/hud/providers/codex-probe.mjs +2 -31
- package/hud/providers/codex.mjs +16 -58
- package/hud/renderers.mjs +22 -31
- package/hud/utils.mjs +0 -8
- package/package.json +1 -1
- package/scripts/__tests__/mcp-guard-engine.test.mjs +3 -3
- package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +1 -2
- package/scripts/doctor-diagnose.mjs +8 -59
- package/scripts/lib/mcp-guard-engine.mjs +2 -3
- package/scripts/mcp-check.mjs +0 -22
- package/scripts/pack.mjs +1 -3
- package/scripts/setup.mjs +5 -5
- package/scripts/tfx-route.sh +1 -2
- package/skills/tfx-lead/SKILL.md +18 -1
- package/skills/tfx-live/SKILL.md +15 -3
- package/skills/tfx-setup/SKILL.md +2 -0
- package/hub/account-broker.mjs +0 -1221
package/CLAUDE.md
CHANGED
|
@@ -81,22 +81,6 @@ Windows 전용 경로는 플랫폼과 `WT_SESSION` 조건을 확인한다.
|
|
|
81
81
|
프로필은 `$CODEX_HOME/<이름>.config.toml` 파일이다(인라인 `[profiles.*]` 아님).
|
|
82
82
|
</codex-config>
|
|
83
83
|
|
|
84
|
-
<account-broker>
|
|
85
|
-
## AccountBroker (계정 브로커)
|
|
86
|
-
|
|
87
|
-
headless 워커는 AccountBroker를 사용한다.
|
|
88
|
-
|
|
89
|
-
| 항목 | 설명 |
|
|
90
|
-
|------|------|
|
|
91
|
-
| 계정별 회로 차단기 | 장애 격리: 한 계정 오류가 다른 계정에 전파되지 않음 |
|
|
92
|
-
| 사용 중 플래그 | 동일 계정 이중 임대 방지 |
|
|
93
|
-
| `reloadBroker()` | accounts.json을 다시 불러온다. 활성 임대 소유권은 다시 불러온 뒤에도 보존한다. 허브가 없어 HTTP 엔드포인트는 없고 모듈 함수로만 부를 수 있다. |
|
|
94
|
-
| 어댑터의 임대 없음 정책 | headless 어댑터는 브로커가 비활성·비어 있음이면 기본 CLI 인증 경로로 실행하고, 브로커가 활성인데 임대가 없으면 `circuit_open`으로 실패한다. |
|
|
95
|
-
| 공개 스냅숏 정책 | 밖에 보여 줄 때는 `publicSnapshot()`만 쓴다. `env`, `authFile`, `profile`, `host`, 파일 경로, 가공하지 않은 실패 시각은 공개하지 않는다. |
|
|
96
|
-
| 진단 이벤트 | `securityViolation`, `authSyncError`는 이벤트로만 내보낸다. 구독하는 쪽이 없으면 기록되지 않는다. |
|
|
97
|
-
| EventEmitter 이벤트 | `lease`, `release`, `cooldown`, `tierFallback`, `circuitOpen`, `circuitClose`, `noAvailableAccounts`: HUD 연동용 |
|
|
98
|
-
</account-broker>
|
|
99
|
-
|
|
100
84
|
<remote>
|
|
101
85
|
## 원격 실행
|
|
102
86
|
|
package/README.ko.md
CHANGED
|
@@ -73,8 +73,9 @@ claude plugin install triflux@triflux
|
|
|
73
73
|
| --- | --- |
|
|
74
74
|
| `/tfx-auto` | 구현, 수정, 리뷰, 병렬 작업의 진입점. 동작은 아래 플래그로 정한다. |
|
|
75
75
|
| `/tfx-live` | Claude↔Codex 라이브 세션. `start`/`ask`/`wait`/`stop`, `peer` 중계, `list-sessions`. |
|
|
76
|
+
| `/tfx-lead` | 여러 Claude, Codex 세션을 지휘하는 리드 역할. 역할별 모델, 지시서, 교차 리뷰, 머지·릴리스 조율. |
|
|
76
77
|
| `/tfx-remote` | SSH 원격 Claude Code 세션 시작, 조회, 재부착, 메시지 전송, 준비 상태 확인, 모니터링, 종료. |
|
|
77
|
-
| `/tfx-setup` |
|
|
78
|
+
| `/tfx-setup` | 설정. 파일 동기화, HUD, Codex와 Antigravity 프로필, MCP. mods는 `tfx setup --mods`로 설치. |
|
|
78
79
|
| `/tfx-doctor` | 진단과 복구. |
|
|
79
80
|
| `/tfx-ship` | triflux 릴리즈 절차(메인테이너용). |
|
|
80
81
|
| `/tfx-wt` | Windows Terminal 탭·패인 조작. `tfx setup`은 Windows에만 설치한다. |
|
package/README.md
CHANGED
|
@@ -75,8 +75,9 @@ Skills you invoke directly:
|
|
|
75
75
|
| --- | --- |
|
|
76
76
|
| `/tfx-auto` | Front door for implementing, fixing, reviewing, and parallel work. Behavior is set by flags (below). |
|
|
77
77
|
| `/tfx-live` | Live Claude↔Codex sessions: `start`/`ask`/`wait`/`stop`, `peer` relay, `list-sessions`. |
|
|
78
|
-
| `/tfx-
|
|
79
|
-
| `/tfx-
|
|
78
|
+
| `/tfx-lead` | Lead role for running several Claude/Codex sessions: roles and models, briefs, cross review, merge and release coordination. |
|
|
79
|
+
| `/tfx-remote` | Remote Claude Code sessions over SSH: start, list, reattach, send, readiness probe, monitor, stop. |
|
|
80
|
+
| `/tfx-setup` | Setup: file sync, HUD, Codex and Antigravity profiles, MCP. `tfx setup --mods` installs mods. |
|
|
80
81
|
| `/tfx-doctor` | Diagnose and repair. |
|
|
81
82
|
| `/tfx-ship` | triflux release flow (maintainers). |
|
|
82
83
|
| `/tfx-wt` | Windows Terminal tabs and panes. `tfx setup` installs it on Windows only. |
|
|
@@ -3,7 +3,7 @@ name: tfx-live
|
|
|
3
3
|
description: >
|
|
4
4
|
Claude·Codex 세션을 실시간으로 생성·질의·대기·압축·종료하거나,
|
|
5
5
|
tmux/daemon UDS 연결과 peer 중계를 운영할 때 사용한다.
|
|
6
|
-
argument-hint: "<start|ask|wait|compact|stop|rename|interrupt|probe|list-sessions|peer|converse|goal-driven> ..."
|
|
6
|
+
argument-hint: "<start|ask|wait|queue|compact|stop|rename|interrupt|probe|list-sessions|peer|converse|goal-driven> ..."
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# tfx-live
|
|
@@ -29,7 +29,7 @@ tfx-live ask --cli codex --session cx1 --prompt "현재 변경사항을 요약
|
|
|
29
29
|
tfx-live stop --cli codex --session cx1
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
새 세션 이름은 `<월>.<일> <주제>`로 짓는다. `start --name`을 생략하면 자동으로 만들고 `nameGenerated: true`를 보고한다. `--resume` 또는 `--resume-last`는 `--name`을 명시한 경우에만 이름을 적용한다. 승계 세션에는 기존 이름 뒤에 ` 2`, 이후 ` 3`을 붙인다. 이름 적용을 요청한 경우 결과의 `nameApplied: false`는 적용 실패를 뜻한다. Codex `start`는 `/rename` 제출을 입력창이 빌 때까지 확인하고, 이름으로 찾은 thread UUID를 `threadId`로 돌려주며 tmux 세션 옵션 `@tfx_codex_thread`에 남긴다. 기존 Codex UDS thread는 `tfx-live rename --cli codex --transport uds --thread ID --name "<이름>"`으로 이름을 바꾼다.
|
|
32
|
+
새 세션 이름은 `<월>.<일> <주제>`로 짓는다. `start --name`을 생략하면 자동으로 만들고 `nameGenerated: true`를 보고한다. `--resume` 또는 `--resume-last`는 `--name`을 명시한 경우에만 이름을 적용한다. 승계 세션에는 기존 이름 뒤에 ` 2`, 이후 ` 3`을 붙인다. 이름 적용을 요청한 경우 결과의 `nameApplied: false`는 적용 실패를 뜻한다. Codex `start`는 `/rename` 제출을 입력창이 빌 때까지 확인하고, 이름으로 찾은 thread UUID를 `threadId`로 돌려주며 tmux 세션 옵션 `@tfx_codex_thread`에 남긴다. 로컬 Codex `--resume-last`는 Codex가 고른 thread를 사후에 찾는다. 띄운 뒤 공유 app-server daemon에 로드된 같은 cwd의 thread 중 rollout에 그 시각 이후의 resume 기록(`thread_settings_applied`)이 생긴 것이 정확히 하나이고 1.5초 뒤에도 같을 때만 `threadId`로 쓴다. 이 확인은 5초 안에 끝난다. TUI가 daemon을 쓰지 않거나 같은 cwd에서 동시에 resume이 일어나면 `null`이다. 기존 Codex UDS thread는 `tfx-live rename --cli codex --transport uds --thread ID --name "<이름>"`으로 이름을 바꾼다.
|
|
33
33
|
|
|
34
34
|
## 리드 운영
|
|
35
35
|
|
|
@@ -51,11 +51,19 @@ tfx-live stop --cli codex --session cx1
|
|
|
51
51
|
1. 기본은 `codex queue --thread <UUID>`다. 입력창을 건드리지 않고, 바쁘면 쌓였다가 앞 턴이 끝난 직후 순서대로 들어간다. 유휴 TUI는 약 20초 주기로 큐를 가져간다.
|
|
52
52
|
2. 첫 줄 머리말은 `[from <보낸 세션 이름>] [tfx-live req=<id>]`다. 이름은 `--from`, `$TFX_LIVE_FROM`, Claude 세션 기록, Codex `session_index` 순으로 찾고 없으면 머리말 없이 표식만 붙인다. Codex TUI는 Claude처럼 접어 보여 주지 않고 사용자 입력으로 그대로 표시한다.
|
|
53
53
|
3. thread는 `--thread UUID`, `start`가 남긴 `@tfx_codex_thread`, Codex 세션 레지스트리, pane 프로세스의 `resume <UUID>` 인자, pane cwd와 같은 rollout 하나 순으로 찾는다. cwd 대응은 그 cwd의 Codex TUI가 하나일 때만 쓰고 exec 워커와 서브에이전트 thread는 뺀다. 이름으로는 보내지 않는다(`codex queue`의 이름 조회가 실패한다).
|
|
54
|
-
4. tmux 직접 입력은 queue를 못 쓸 때만 쓴다: 원격 호스트, thread를 못 찾음(`resume --last`로 띄운 세션, 같은 cwd의 TUI 여럿 포함), `codex queue` 오류(0.160 미만 포함). 결과에 `transport: "tmux"`, `transportRequested: "queue"`, `fallbackReason`이 남는다. 시간 초과처럼 queue가 이미 쌓았을 수도 있는 오류는 `status: "unknown"`으로 끝내고 재전송하지 않는다.
|
|
54
|
+
4. tmux 직접 입력은 queue를 못 쓸 때만 쓴다: 원격 호스트, thread를 못 찾음(`start` 밖에서 `resume --last`로 띄운 세션, `start --resume-last`의 `threadId: null`, 같은 cwd의 TUI 여럿 포함), `codex queue` 오류(0.160 미만 포함). 결과에 `transport: "tmux"`, `transportRequested: "queue"`, `fallbackReason`이 남는다. 시간 초과처럼 queue가 이미 쌓았을 수도 있는 오류는 `status: "unknown"`으로 끝내고 재전송하지 않는다.
|
|
55
55
|
5. 슬래시 명령(`/rename`, `/compact`, `/new`)과 interrupt(Escape)는 tmux 직접 입력 전용이다. queue로 보낸 슬래시 명령은 실행되지 않고 일반 텍스트로 모델에 들어간다. `/명령`으로 시작하는 프롬프트(절대 경로는 제외)와 `--if-busy fail|interrupt`는 tmux로 보내고 `transportReason`을 남긴다. 슬래시 명령에는 표식을 붙이지 않으므로 `wait`로 추적할 수 없다.
|
|
56
56
|
|
|
57
57
|
결과의 `status`는 `queued`(쌓임), `working`(TUI가 가져가 턴 시작), `completed`, `failed`로 나뉘고 `delivered`, `deliveredAt`, `turnId`가 배달을 구분한다. queue는 바쁜 세션에도 쌓으므로 `--if-busy wait`는 의미가 없다.
|
|
58
58
|
|
|
59
|
+
```bash
|
|
60
|
+
tfx-live queue --cli codex --session cx1
|
|
61
|
+
tfx-live queue --cli codex --session cx1 --request-id <requestId> --delete all
|
|
62
|
+
tfx-live queue --cli codex --thread <UUID> --delete <queuedMessageId>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`queue`는 TUI가 아직 가져가지 않은 항목을 보고 지운다. Codex app-server의 실험 API `thread/queue/list`·`thread/queue/delete`를 stdio로 호출하며 큐 저장소 파일은 건드리지 않는다. 목록 항목은 `id`(ask 결과의 `queuedMessageId`와 같다), `requestId`, `text`다. 삭제 결과의 `missing`은 이미 TUI가 가져갔거나 없는 항목이다. 유휴 TUI는 쌓인 항목을 곧 가져가므로 지울 수 있는 것은 사실상 바쁜 세션과 닫힌 세션의 항목이다. 가져간 뒤의 처리 여부는 `wait`로 확인한다.
|
|
66
|
+
|
|
59
67
|
### 컨텍스트 판단
|
|
60
68
|
|
|
61
69
|
분모는 공식 모델 창이고 실행 한도가 더 작으면 그 한도 아래에서 운영한다. 아래 비율은 공식 권고가 아닌 **[추론] 운영값**이다. 모델 문서가 바뀌면 한도도 갱신한다.
|
|
@@ -90,6 +98,8 @@ tfx-live stop --session cl1
|
|
|
90
98
|
```bash
|
|
91
99
|
tfx-live ask --cli claude --transport uds --short <8hex> --prompt "진행 상황을 보고해줘" --no-wait
|
|
92
100
|
tfx-live wait --cli claude --short <8hex> --request-id <requestId> --timeout 120 --poll-interval 500
|
|
101
|
+
tfx-live ask --cli claude --session cl1 --prompt "진행 상황을 보고해줘" --no-wait
|
|
102
|
+
tfx-live wait --cli claude --session cl1 --request-id <requestId> --timeout 120
|
|
93
103
|
tfx-live ask --cli codex --session cx1 --prompt "진행 상황을 보고해줘" --no-wait
|
|
94
104
|
tfx-live wait --cli codex --session cx1 --request-id <requestId> --timeout 120
|
|
95
105
|
```
|
|
@@ -98,6 +108,8 @@ tfx-live wait --cli codex --session cx1 --request-id <requestId> --timeout 120
|
|
|
98
108
|
|
|
99
109
|
`wait`는 Claude transcript에서 표식을 user 메시지, `queue-operation`의 `enqueue` content, queued command attachment에서 찾는다. 표식이 소비된 턴이 끝난 뒤 마지막 assistant 텍스트를 응답으로 쓴다. 표식 없는 `wait`는 최신 user turn을 본다. `isSidechain`과 합성 API 오류 메시지는 정상 응답에서 제외한다. 요청을 소비한 턴에서 합성 오류가 나타나면 오류 문구와 `status: "failed"`를 반환한다. daemon의 `idle`만으로 완료를 단정하지 않는다. timeout은 `status: "working"`, `timedOut: true`, `done: false`다.
|
|
100
110
|
|
|
111
|
+
tmux로 띄운 대화형 Claude는 `wait --session <tmux 대상>`을 쓴다. pane에 붙은 Claude 세션 기록(`~/.claude/sessions`)에서 sessionId를 찾아 같은 transcript 규칙으로 판정하고, daemon의 idle 대신 세션 기록의 `status: "idle"`을 쓴다. transcript는 처음 찾은 것을 끝까지 쓰므로 대기 중 `/clear`로 바뀐 세션은 따라가지 않는다. 로컬 전용이다.
|
|
112
|
+
|
|
101
113
|
Codex `wait`는 `--session` 또는 `--thread UUID`의 rollout에서 표식이 든 user 메시지의 `turn_id`를 찾고, 같은 턴의 `task_complete.last_agent_message`를 응답으로 쓴다. `turn_aborted`는 `status: "failed"`다. 표식이 아직 없으면 `status: "queued"`, `timedOut: true`다. `--timeout 0`은 한 번만 확인한다.
|
|
102
114
|
|
|
103
115
|
명시한 `--config-dir`은 정확히 일치하는 daemon만 선택한다. `CLAUDE_CONFIG_DIR`도 해당 env 디렉터리로 제한한다. stale endpoint 복구는 같은 source configDir 안에서만 재시도한다. attach 전 예외는 `inputSent: false`이므로 `auto`의 tmux fallback이 가능하다. attach 뒤 전송 여부가 불명확하면 `status: "unknown"`으로 보고하고 재전송하지 않는다.
|
package/bin/tfx-live.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
readdir,
|
|
9
9
|
readFile,
|
|
10
10
|
rm,
|
|
11
|
+
stat,
|
|
11
12
|
writeFile,
|
|
12
13
|
} from "node:fs/promises";
|
|
13
14
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -22,14 +23,17 @@ import {
|
|
|
22
23
|
import { resolveHardCeilingMs } from "../hub/lib/worker-lifecycle.mjs";
|
|
23
24
|
import { exposeLiveSession } from "../hub/team/agents-row.mjs";
|
|
24
25
|
import {
|
|
26
|
+
claudeWaitVerdict,
|
|
25
27
|
findClaudeTranscript,
|
|
26
28
|
readClaudeTranscript,
|
|
27
29
|
} from "../hub/team/claude-transcript.mjs";
|
|
28
30
|
import {
|
|
29
31
|
codexThreadIdByName,
|
|
30
32
|
countCodexTuiInCwd,
|
|
33
|
+
deleteCodexQueueItems,
|
|
31
34
|
findCodexThreadByCwd,
|
|
32
35
|
isCodexThreadId,
|
|
36
|
+
listCodexQueue,
|
|
33
37
|
queueCodexMessage,
|
|
34
38
|
resolveSenderName,
|
|
35
39
|
waitCodexRequest,
|
|
@@ -43,6 +47,7 @@ import {
|
|
|
43
47
|
import {
|
|
44
48
|
CONTEXT_THRESHOLDS,
|
|
45
49
|
contextGuard,
|
|
50
|
+
findCodexRollout,
|
|
46
51
|
modelContext,
|
|
47
52
|
readCodexContext,
|
|
48
53
|
} from "../hub/team/session-context.mjs";
|
|
@@ -94,8 +99,10 @@ function usage(command) {
|
|
|
94
99
|
" tfx-live ask --transport uds|auto (--short SHORT | --session-id ID) --prompt TEXT [--config-dir DIR] [--bridge ABS] [--session NAME (auto fallback)] [--timeout 60]",
|
|
95
100
|
" ask options: --no-wait --no-relay-tag --warn-context-pct N --max-context-pct N (0 disables; Claude 60/90, Codex 15/22).",
|
|
96
101
|
" tfx-live compact --cli claude --session NAME [--instructions TEXT] [--if-busy fail|wait] [--timeout 60]",
|
|
97
|
-
" tfx-live wait --cli claude (--short SHORT | --session-id ID) [--request-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 60] [--poll-interval 1500]",
|
|
102
|
+
" tfx-live wait --cli claude (--short SHORT | --session-id ID | --session NAME[:WINDOW.PANE]) [--request-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 60] [--poll-interval 1500]",
|
|
98
103
|
" tfx-live wait --cli codex (--session NAME[:WINDOW.PANE] | --thread UUID) --request-id ID [--timeout 60] [--poll-interval 1500]",
|
|
104
|
+
" tfx-live queue --cli codex (--session NAME[:WINDOW.PANE] | --thread UUID) [--request-id ID] [--delete QUEUED_ID|all]",
|
|
105
|
+
" queue lists or deletes items the TUI has not picked up yet (official app-server thread/queue API); missing means already delivered or gone.",
|
|
99
106
|
" tfx-live rename --cli codex --transport uds --thread ID --name NAME [--codex-socket PATH|default]",
|
|
100
107
|
" transport: auto is the default for Claude when --short/--session-id is present; otherwise tmux. bridge path: --bridge > $TFX_BRIDGE > $TFX_REPO_ROOT/hub/bridge.mjs > bundled Triflux hub/bridge.mjs.",
|
|
101
108
|
" tfx-live interrupt --session NAME [--cli codex|claude] [--transport tmux|uds|auto] [--short SHORT | --session-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 5]",
|
|
@@ -2061,6 +2068,76 @@ async function resolveAskTransport(adapter, opts, deps = {}) {
|
|
|
2061
2068
|
};
|
|
2062
2069
|
}
|
|
2063
2070
|
|
|
2071
|
+
// 공유 app-server daemon 에 로드된 thread 중 cwd 가 같은 것. daemon 이 없거나 늦으면 빈 목록.
|
|
2072
|
+
async function loadedCodexThreadIds(cwd, timeoutMs) {
|
|
2073
|
+
try {
|
|
2074
|
+
const { listCodexAppServerThreads } = await import(
|
|
2075
|
+
"../hub/team/uds-orchestrator.mjs"
|
|
2076
|
+
);
|
|
2077
|
+
const threads = await listCodexAppServerThreads({
|
|
2078
|
+
socketPath: resolveCodexDaemonSocket("default"),
|
|
2079
|
+
cwd,
|
|
2080
|
+
timeoutMs,
|
|
2081
|
+
});
|
|
2082
|
+
return threads.map((thread) => thread.threadId);
|
|
2083
|
+
} catch {
|
|
2084
|
+
return [];
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
const ROLLOUT_TAIL_BYTES = 256 * 1024;
|
|
2089
|
+
|
|
2090
|
+
// thread_settings_applied 는 resume 과 설정 변경 때 쓰인다. 끝부분이 sinceMs 이후를 다 덮지
|
|
2091
|
+
// 못하면 "unknown" 이다.
|
|
2092
|
+
async function resumedSince(rollout, sinceMs) {
|
|
2093
|
+
const info = await stat(rollout).catch(() => null);
|
|
2094
|
+
if (!info || info.mtimeMs < sinceMs) return "no";
|
|
2095
|
+
const handle = await open(rollout, "r");
|
|
2096
|
+
let text;
|
|
2097
|
+
try {
|
|
2098
|
+
const length = Math.min(info.size, ROLLOUT_TAIL_BYTES);
|
|
2099
|
+
const { buffer } = await handle.read(
|
|
2100
|
+
Buffer.alloc(length),
|
|
2101
|
+
0,
|
|
2102
|
+
length,
|
|
2103
|
+
info.size - length,
|
|
2104
|
+
);
|
|
2105
|
+
text = buffer.toString("utf8");
|
|
2106
|
+
} finally {
|
|
2107
|
+
await handle.close();
|
|
2108
|
+
}
|
|
2109
|
+
let covered = info.size <= ROLLOUT_TAIL_BYTES;
|
|
2110
|
+
for (const line of text.split("\n")) {
|
|
2111
|
+
let entry;
|
|
2112
|
+
try {
|
|
2113
|
+
entry = JSON.parse(line);
|
|
2114
|
+
} catch {
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
const at = Date.parse(entry.timestamp);
|
|
2118
|
+
if (at < sinceMs) covered = true;
|
|
2119
|
+
else if (entry.payload?.type === "thread_settings_applied")
|
|
2120
|
+
return "resumed";
|
|
2121
|
+
}
|
|
2122
|
+
return covered ? "no" : "unknown";
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// resume --last 는 pane 인자에 UUID 가 남지 않아 Codex 가 실제로 연 thread 를 사후에 찾는다.
|
|
2126
|
+
// 띄운 뒤 기록이 생긴 thread 가 정확히 하나이고 나머지 후보가 모두 확인됐을 때만 인정한다.
|
|
2127
|
+
async function resumedCodexThread(
|
|
2128
|
+
{ cwd, launchedAtMs, timeoutMs },
|
|
2129
|
+
{ loadedIds = loadedCodexThreadIds, findRollout = findCodexRollout } = {},
|
|
2130
|
+
) {
|
|
2131
|
+
const matches = [];
|
|
2132
|
+
for (const id of await loadedIds(cwd, timeoutMs)) {
|
|
2133
|
+
const rollout = isCodexThreadId(id) && (await findRollout(id));
|
|
2134
|
+
const state = rollout ? await resumedSince(rollout, launchedAtMs) : "no";
|
|
2135
|
+
if (state === "unknown") return null;
|
|
2136
|
+
if (state === "resumed") matches.push(id);
|
|
2137
|
+
}
|
|
2138
|
+
return matches.length === 1 ? matches[0] : null;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2064
2141
|
async function doStart(adapter, opts) {
|
|
2065
2142
|
const {
|
|
2066
2143
|
session,
|
|
@@ -2093,6 +2170,8 @@ async function doStart(adapter, opts) {
|
|
|
2093
2170
|
if (resumed && name && adapter.cli === "claude")
|
|
2094
2171
|
launchKeys[0] += ` ${shellQuote(["-n", name])}`;
|
|
2095
2172
|
const resumeTarget = resume ?? (resumeLast ? "last" : null);
|
|
2173
|
+
const trackResumeLast = resumeLast && adapter.cli === "codex" && !remote;
|
|
2174
|
+
const launchedAtMs = Date.now();
|
|
2096
2175
|
|
|
2097
2176
|
// 분리 세션 기본 80x24 에서는 긴 입력이 화면 높이를 넘으므로 넓게 만든다. 붙으면 클라이언트 크기를 따른다.
|
|
2098
2177
|
await runTmux(remote, [
|
|
@@ -2129,6 +2208,23 @@ async function doStart(adapter, opts) {
|
|
|
2129
2208
|
let nameApplied = Boolean(name) && adapter.cli === "claude" && ready;
|
|
2130
2209
|
let threadId =
|
|
2131
2210
|
adapter.cli === "codex" && isCodexThreadId(resume) ? resume : null;
|
|
2211
|
+
// 우리 TUI 의 resume 기록이 늦게 반영되면 남의 기록만 보일 수 있어, 1.5초 뒤에도 같은 후보가
|
|
2212
|
+
// 유일할 때만 확정한다. 5초 안에 확인하지 못하면 null 로 둔다.
|
|
2213
|
+
const threadDeadline = Date.now() + 5000;
|
|
2214
|
+
let candidate = null;
|
|
2215
|
+
while (trackResumeLast && ready && !threadId) {
|
|
2216
|
+
const timeoutMs = threadDeadline - Date.now();
|
|
2217
|
+
if (timeoutMs <= 0) break;
|
|
2218
|
+
const found = await resumedCodexThread({
|
|
2219
|
+
cwd: cwd ?? process.cwd(),
|
|
2220
|
+
launchedAtMs,
|
|
2221
|
+
timeoutMs,
|
|
2222
|
+
});
|
|
2223
|
+
if (found && found === candidate) threadId = found;
|
|
2224
|
+
candidate = found;
|
|
2225
|
+
if (!threadId)
|
|
2226
|
+
await sleep(Math.min(found ? 1500 : 500, threadDeadline - Date.now()));
|
|
2227
|
+
}
|
|
2132
2228
|
if (name && adapter.cli === "codex" && ready) {
|
|
2133
2229
|
try {
|
|
2134
2230
|
const command = `/rename ${name}`;
|
|
@@ -3272,19 +3368,24 @@ async function ask(flags) {
|
|
|
3272
3368
|
printJson(await doAsk(adapter, askOpts(flags, adapter)));
|
|
3273
3369
|
}
|
|
3274
3370
|
|
|
3371
|
+
async function codexThreadFromFlags(flags) {
|
|
3372
|
+
if (flags.thread) {
|
|
3373
|
+
if (!isCodexThreadId(flags.thread))
|
|
3374
|
+
throw new Error("--thread must be a Codex session UUID");
|
|
3375
|
+
return { threadId: flags.thread, threadSource: "flag" };
|
|
3376
|
+
}
|
|
3377
|
+
const thread = await resolveCodexTmuxThread(
|
|
3378
|
+
splitTmuxTarget(requireFlag(flags, "session")).target,
|
|
3379
|
+
);
|
|
3380
|
+
if (!thread.threadId)
|
|
3381
|
+
throw new Error(`Codex thread unavailable: ${thread.reason}`);
|
|
3382
|
+
return thread;
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3275
3385
|
async function waitCodex(flags) {
|
|
3276
3386
|
if (flags.remote) throw new Error("Codex wait supports local sessions only");
|
|
3277
3387
|
const requestId = requireFlag(flags, "request-id");
|
|
3278
|
-
|
|
3279
|
-
if (flags.thread && !isCodexThreadId(flags.thread))
|
|
3280
|
-
throw new Error("--thread must be a Codex session UUID");
|
|
3281
|
-
if (!flags.thread) {
|
|
3282
|
-
thread = await resolveCodexTmuxThread(
|
|
3283
|
-
splitTmuxTarget(requireFlag(flags, "session")).target,
|
|
3284
|
-
);
|
|
3285
|
-
if (!thread.threadId)
|
|
3286
|
-
throw new Error(`Codex thread unavailable: ${thread.reason}`);
|
|
3287
|
-
}
|
|
3388
|
+
const thread = await codexThreadFromFlags(flags);
|
|
3288
3389
|
printJson({
|
|
3289
3390
|
...(await waitCodexRequest({
|
|
3290
3391
|
threadId: thread.threadId,
|
|
@@ -3296,12 +3397,74 @@ async function waitCodex(flags) {
|
|
|
3296
3397
|
});
|
|
3297
3398
|
}
|
|
3298
3399
|
|
|
3400
|
+
// tmux 대화형 Claude 는 daemon 이 없어 pane 의 세션 레코드 status 와 transcript 로 판정한다.
|
|
3401
|
+
async function waitClaudeTmux(flags) {
|
|
3402
|
+
if (flags.remote)
|
|
3403
|
+
throw new Error("Claude tmux wait supports local sessions only");
|
|
3404
|
+
const session = splitTmuxTarget(flags.session).target;
|
|
3405
|
+
const requestId = flags["request-id"] ?? null;
|
|
3406
|
+
const timeoutMs = secondsFlag(flags, "timeout", DEFAULT_ANSWER_TIMEOUT_MS);
|
|
3407
|
+
const pollIntervalMs = msFlag(
|
|
3408
|
+
flags,
|
|
3409
|
+
"poll-interval",
|
|
3410
|
+
DEFAULT_POLL_INTERVAL_MS,
|
|
3411
|
+
);
|
|
3412
|
+
const deadline = Date.now() + timeoutMs;
|
|
3413
|
+
let transcriptPath = null;
|
|
3414
|
+
for (;;) {
|
|
3415
|
+
const target = await claudeTmuxTarget(session);
|
|
3416
|
+
if (!target)
|
|
3417
|
+
throw new Error(`no live Claude session in tmux target ${session}`);
|
|
3418
|
+
// 처음 찾은 transcript 를 고정해 /clear 로 바뀐 세션을 따라가지 않는다.
|
|
3419
|
+
transcriptPath ??= await claudeTmuxTranscript(
|
|
3420
|
+
session,
|
|
3421
|
+
flags["config-dir"],
|
|
3422
|
+
target,
|
|
3423
|
+
);
|
|
3424
|
+
const transcript = await readClaudeTranscript(transcriptPath, {
|
|
3425
|
+
requestId,
|
|
3426
|
+
});
|
|
3427
|
+
const result = {
|
|
3428
|
+
ok: true,
|
|
3429
|
+
status: transcript?.userSeen ? "working" : "submitted",
|
|
3430
|
+
done: false,
|
|
3431
|
+
timedOut: false,
|
|
3432
|
+
response: transcript?.response || "",
|
|
3433
|
+
...(transcript?.context ?? modelContext("claude", null)),
|
|
3434
|
+
requestId,
|
|
3435
|
+
target,
|
|
3436
|
+
};
|
|
3437
|
+
const verdict = claudeWaitVerdict(transcript, target.status === "idle");
|
|
3438
|
+
if (verdict) return { ...result, ...verdict };
|
|
3439
|
+
if (Date.now() >= deadline) return { ...result, timedOut: true };
|
|
3440
|
+
await sleep(Math.min(pollIntervalMs, deadline - Date.now()));
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
// 배달 전 항목은 공식 app-server 큐 API 로만 보고 지운다. 큐 저장소 파일은 건드리지 않는다.
|
|
3445
|
+
async function queue(flags) {
|
|
3446
|
+
if ((flags.cli ?? "codex") !== "codex" || flags.remote)
|
|
3447
|
+
throw new Error("queue supports local --cli codex only");
|
|
3448
|
+
const { threadId, threadSource } = await codexThreadFromFlags(flags);
|
|
3449
|
+
const requestId = flags["request-id"];
|
|
3450
|
+
const items = (await listCodexQueue(threadId)).filter(
|
|
3451
|
+
(item) => !requestId || item.requestId === requestId,
|
|
3452
|
+
);
|
|
3453
|
+
const result = { ok: true, cli: "codex", threadId, threadSource };
|
|
3454
|
+
if (!flags.delete) return printJson({ ...result, items });
|
|
3455
|
+
const ids =
|
|
3456
|
+
flags.delete === "all" ? items.map((item) => item.id) : [flags.delete];
|
|
3457
|
+
printJson({ ...result, ...(await deleteCodexQueueItems(threadId, ids)) });
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3299
3460
|
async function wait(flags) {
|
|
3300
3461
|
if (flags.cli === "codex") return waitCodex(flags);
|
|
3301
3462
|
if ((flags.cli ?? "claude") !== "claude")
|
|
3302
3463
|
throw new Error("wait supports --cli claude or codex");
|
|
3464
|
+
if (flags.session && !flags.short && !flags["session-id"])
|
|
3465
|
+
return printJson(await waitClaudeTmux(flags));
|
|
3303
3466
|
if (!flags.short && !flags["session-id"])
|
|
3304
|
-
throw new Error("wait requires --short or --session
|
|
3467
|
+
throw new Error("wait requires --short, --session-id or --session");
|
|
3305
3468
|
if (flags.remote || (flags.transport && flags.transport !== "uds"))
|
|
3306
3469
|
throw new Error("wait supports local Claude UDS only");
|
|
3307
3470
|
const timeoutMs = secondsFlag(flags, "timeout", DEFAULT_ANSWER_TIMEOUT_MS);
|
|
@@ -3354,7 +3517,7 @@ async function stop(flags) {
|
|
|
3354
3517
|
printJson(await doStop(adapter, stopOpts(flags)));
|
|
3355
3518
|
}
|
|
3356
3519
|
|
|
3357
|
-
async function
|
|
3520
|
+
async function claudeTmuxTarget(session) {
|
|
3358
3521
|
const { stdout: paneId } = await runTmux(null, [
|
|
3359
3522
|
"display-message",
|
|
3360
3523
|
"-p",
|
|
@@ -3363,9 +3526,13 @@ async function claudeTmuxTranscript(session, configDir) {
|
|
|
3363
3526
|
"#{pane_id}",
|
|
3364
3527
|
]);
|
|
3365
3528
|
const discovery = await discoverClaudeTmuxSessions();
|
|
3366
|
-
|
|
3367
|
-
(entry) => entry.paneId === paneId.trim()
|
|
3529
|
+
return (
|
|
3530
|
+
discovery.sessions.find((entry) => entry.paneId === paneId.trim()) ?? null
|
|
3368
3531
|
);
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
async function claudeTmuxTranscript(session, configDir, target) {
|
|
3535
|
+
target ??= await claudeTmuxTarget(session);
|
|
3369
3536
|
return findClaudeTranscript({
|
|
3370
3537
|
configDir:
|
|
3371
3538
|
configDir ||
|
|
@@ -4389,6 +4556,8 @@ async function main() {
|
|
|
4389
4556
|
await ask(flags);
|
|
4390
4557
|
} else if (command === "wait") {
|
|
4391
4558
|
await wait(flags);
|
|
4559
|
+
} else if (command === "queue") {
|
|
4560
|
+
await queue(flags);
|
|
4392
4561
|
} else if (command === "compact") {
|
|
4393
4562
|
await compact(flags);
|
|
4394
4563
|
} else if (command === "rename") {
|
|
@@ -4437,6 +4606,7 @@ export {
|
|
|
4437
4606
|
peerSideBaseOpts,
|
|
4438
4607
|
resolveAskTransport,
|
|
4439
4608
|
resolveCodexDaemonSocket,
|
|
4609
|
+
resumedCodexThread,
|
|
4440
4610
|
splitTmuxTarget,
|
|
4441
4611
|
stopOpts,
|
|
4442
4612
|
tmuxBufferName,
|
package/bin/triflux.mjs
CHANGED
|
@@ -154,8 +154,7 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
154
154
|
{
|
|
155
155
|
name: "--diagnose",
|
|
156
156
|
type: "boolean",
|
|
157
|
-
description:
|
|
158
|
-
"진단 번들(zip) 생성: spawn-trace + hook timing + system info",
|
|
157
|
+
description: "진단 번들(zip) 생성: spawn-trace + system info",
|
|
159
158
|
},
|
|
160
159
|
{
|
|
161
160
|
name: "--purge-logs",
|
|
@@ -1318,7 +1317,7 @@ function cmdSetup(options = {}) {
|
|
|
1318
1317
|
summary.push({ item: "Codex profiles", status: "⚠️", detail: reason });
|
|
1319
1318
|
} else if (codexProfileResult.changed > 0) {
|
|
1320
1319
|
ok(
|
|
1321
|
-
`Codex profiles: ${codexProfileResult.changed}개 반영됨 (~/.codex
|
|
1320
|
+
`Codex profiles: ${codexProfileResult.changed}개 반영됨 (~/.codex/<프로필>.config.toml)`,
|
|
1322
1321
|
);
|
|
1323
1322
|
summary.push({
|
|
1324
1323
|
item: "Codex profiles",
|
|
@@ -1671,7 +1670,8 @@ async function cmdDoctor(options = {}) {
|
|
|
1671
1670
|
name: "mcp-inventory",
|
|
1672
1671
|
status: "failed",
|
|
1673
1672
|
});
|
|
1674
|
-
warn("MCP 인벤토리 재생성 실패
|
|
1673
|
+
warn("MCP 인벤토리 재생성 실패");
|
|
1674
|
+
info(`수동: node ${mcpCheck}`);
|
|
1675
1675
|
}
|
|
1676
1676
|
}
|
|
1677
1677
|
const hudScript = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
@@ -2126,7 +2126,7 @@ async function cmdDoctor(options = {}) {
|
|
|
2126
2126
|
);
|
|
2127
2127
|
if (psmuxSupport.missingOptionalCommands.includes("detach-client")) {
|
|
2128
2128
|
info(
|
|
2129
|
-
" detach-client: WT 1.24 ConPTY close-race 회피용. WT
|
|
2129
|
+
" detach-client: WT 1.24 ConPTY close-race 회피용. WT 탭에서 psmux 세션을 닫을 때 pane freeze/ConPTY hang 위험 증가.",
|
|
2130
2130
|
);
|
|
2131
2131
|
info(
|
|
2132
2132
|
" 해결: psmux v3.4+ 로 업그레이드. 현재 psmux 업그레이드 명령:",
|
|
@@ -2389,17 +2389,12 @@ async function cmdDoctor(options = {}) {
|
|
|
2389
2389
|
status: "ok",
|
|
2390
2390
|
path: mcpCache,
|
|
2391
2391
|
codex_servers: inv.codex?.servers?.length || 0,
|
|
2392
|
-
gemini_servers: inv.gemini?.servers?.length || 0,
|
|
2393
2392
|
});
|
|
2394
2393
|
ok(`캐시 존재 (${inv.timestamp})`);
|
|
2395
2394
|
if (inv.codex?.servers?.length) {
|
|
2396
2395
|
const names = inv.codex.servers.map((s) => s.name).join(", ");
|
|
2397
2396
|
info(`Codex: ${inv.codex.servers.length}개 서버 (${names})`);
|
|
2398
2397
|
}
|
|
2399
|
-
if (inv.gemini?.servers?.length) {
|
|
2400
|
-
const names = inv.gemini.servers.map((s) => s.name).join(", ");
|
|
2401
|
-
info(`Gemini: ${inv.gemini.servers.length}개 서버 (${names})`);
|
|
2402
|
-
}
|
|
2403
2398
|
} catch {
|
|
2404
2399
|
addDoctorCheck(report, {
|
|
2405
2400
|
name: "mcp-inventory",
|
|
@@ -2416,7 +2411,7 @@ async function cmdDoctor(options = {}) {
|
|
|
2416
2411
|
path: mcpCache,
|
|
2417
2412
|
fix: `node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`,
|
|
2418
2413
|
});
|
|
2419
|
-
warn("캐시
|
|
2414
|
+
warn("캐시 없음: tfx update 또는 tfx doctor --reset 이 생성");
|
|
2420
2415
|
info(`수동: node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`);
|
|
2421
2416
|
}
|
|
2422
2417
|
|
|
@@ -3704,16 +3699,22 @@ function cmdHelp() {
|
|
|
3704
3699
|
${DIM} --fix${RESET} ${GRAY}진단 + 자동 수정${RESET}
|
|
3705
3700
|
${DIM} --reset${RESET} ${GRAY}캐시 전체 초기화${RESET}
|
|
3706
3701
|
${DIM} --json${RESET} ${GRAY}구조화된 진단 결과 JSON 출력${RESET}
|
|
3702
|
+
${DIM} --help${RESET} ${GRAY}--audit, --diagnose 등 전체 옵션${RESET}
|
|
3707
3703
|
${WHITE_BRIGHT}tfx mcp${RESET} ${GRAY}MCP registry 관리 (list/sync/add/remove)${RESET}
|
|
3708
3704
|
${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 안정 버전으로 업데이트${RESET}
|
|
3709
3705
|
${DIM} --dev / dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
|
|
3710
3706
|
${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
|
|
3711
|
-
${WHITE_BRIGHT}tfx multi${RESET}
|
|
3707
|
+
${WHITE_BRIGHT}tfx multi${RESET} ${GRAY}멀티-CLI 팀 모드 (tmux)${RESET}
|
|
3708
|
+
${WHITE_BRIGHT}tfx cto${RESET} ${GRAY}저장소 스냅숏 수집과 조회 (collect/status/hygiene)${RESET}
|
|
3712
3709
|
${WHITE_BRIGHT}tfx version${RESET} ${GRAY}버전 표시${RESET}
|
|
3710
|
+
${WHITE_BRIGHT}tfx-live${RESET} ${GRAY}Claude·Codex 라이브 세션 (tfx-live --help)${RESET}
|
|
3713
3711
|
|
|
3714
3712
|
${BOLD}Skills${RESET} ${GRAY}(Claude Code 슬래시 커맨드)${RESET}
|
|
3715
3713
|
|
|
3716
3714
|
${AMBER}/tfx-auto${RESET} ${GRAY}자동 분류 + 병렬 실행 (--cli codex|antigravity|claude, --parallel N)${RESET}
|
|
3715
|
+
${AMBER}/tfx-live${RESET} ${GRAY}Claude·Codex 라이브 세션 생성, 질의, 대기, 종료${RESET}
|
|
3716
|
+
${AMBER}/tfx-lead${RESET} ${GRAY}여러 세션을 지휘하는 리드 역할${RESET}
|
|
3717
|
+
${AMBER}/tfx-remote${RESET} ${GRAY}SSH 원격 Claude Code 세션${RESET}
|
|
3717
3718
|
${AMBER}/tfx-setup${RESET} ${GRAY}HUD 설정 + 진단${RESET}
|
|
3718
3719
|
${YELLOW}/tfx-doctor${RESET} ${GRAY}진단 + 수리 + 캐시 초기화${RESET}
|
|
3719
3720
|
|
|
@@ -3799,9 +3800,7 @@ async function main() {
|
|
|
3799
3800
|
console.log(
|
|
3800
3801
|
`\n ${GREEN_BRIGHT}✓${RESET} 진단 번들 생성: ${result.zipPath}`,
|
|
3801
3802
|
);
|
|
3802
|
-
console.log(
|
|
3803
|
-
` spawn 이벤트: ${result.traceCount}건, 훅 타이밍: ${result.hookTimingCount}건\n`,
|
|
3804
|
-
);
|
|
3803
|
+
console.log(` spawn 이벤트: ${result.traceCount}건\n`);
|
|
3805
3804
|
} else {
|
|
3806
3805
|
console.log(`\n ${RED}✗${RESET} 진단 실패: ${result.error}\n`);
|
|
3807
3806
|
}
|
package/config/mcp-registry.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"transport": "Server transport accepts \"http\" for direct Streamable HTTP MCP endpoints, or \"stdio\" for directly launched MCP servers.",
|
|
11
11
|
"headers": "Optional headers are allowed only for HTTP-compatible transports. Each header value must be a descriptor: {\"value\":\"literal\"} for non-secret static values, {\"env\":\"ENV_VAR_NAME\"} for secrets resolved at sync/runtime, or {\"env\":\"ENV_VAR_NAME\",\"prefix\":\"Bearer \"} for common authorization formats.",
|
|
12
12
|
"secret_safety": "Resolved secret values must not be written back to this registry file. Missing env vars warn during sync and do not emit empty secret headers.",
|
|
13
|
-
"sync_denylist": "Array of client:server strings skipped by proactive registry sync, for example
|
|
13
|
+
"sync_denylist": "Array of client:server strings skipped by proactive registry sync, for example codex:context7."
|
|
14
14
|
},
|
|
15
15
|
"servers": {
|
|
16
16
|
"context7": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"transport": "http",
|
|
19
19
|
"url": "https://mcp.context7.com/mcp",
|
|
20
20
|
"safe": true,
|
|
21
|
-
"targets": ["claude", "
|
|
21
|
+
"targets": ["claude", "codex", "antigravity"],
|
|
22
22
|
"description": "Upstash Context7 — 라이브러리 문서/코드 컨텍스트 (HTTP MCP, API key 불필요)"
|
|
23
23
|
},
|
|
24
24
|
"exa": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"Authorization": { "env": "EXA_API_KEY", "prefix": "Bearer " }
|
|
30
30
|
},
|
|
31
31
|
"safe": true,
|
|
32
|
-
"targets": ["claude", "
|
|
32
|
+
"targets": ["claude", "codex", "antigravity"],
|
|
33
33
|
"description": "Exa neural/semantic web search — 학술/기술 깊이. Key 발급: https://exa.ai/dashboard → secrets.env의 EXA_API_KEY"
|
|
34
34
|
},
|
|
35
35
|
"brave-search": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"BRAVE_API_KEY": { "env": "BRAVE_API_KEY" }
|
|
42
42
|
},
|
|
43
43
|
"safe": true,
|
|
44
|
-
"targets": ["claude", "
|
|
44
|
+
"targets": ["claude", "codex", "antigravity"],
|
|
45
45
|
"description": "Brave Search MCP — 직접 stdio 연결. secrets.env 의 BRAVE_API_KEY 참조."
|
|
46
46
|
},
|
|
47
47
|
"tavily": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"Authorization": { "env": "TAVILY_API_KEY", "prefix": "Bearer " }
|
|
53
53
|
},
|
|
54
54
|
"safe": true,
|
|
55
|
-
"targets": ["claude", "
|
|
55
|
+
"targets": ["claude", "codex", "antigravity"],
|
|
56
56
|
"description": "Tavily research — 비용/운영/DX/일반 웹. Key 발급: https://app.tavily.com/home → secrets.env의 TAVILY_API_KEY"
|
|
57
57
|
}
|
|
58
58
|
},
|
|
@@ -60,7 +60,6 @@
|
|
|
60
60
|
"unknown_server_action": "warn",
|
|
61
61
|
"sync_denylist": [],
|
|
62
62
|
"watched_paths": [
|
|
63
|
-
"~/.gemini/settings.json",
|
|
64
63
|
"~/.codex/config.toml",
|
|
65
64
|
"~/.claude.json",
|
|
66
65
|
"~/.claude/settings.json",
|
package/hub/bridge.mjs
CHANGED
|
@@ -465,9 +465,8 @@ async function cmdDaemonWait(args) {
|
|
|
465
465
|
if (!payload.short && !payload.sessionId)
|
|
466
466
|
throw new Error("short or sessionId is required");
|
|
467
467
|
const { probeClaudeDaemonCandidates } = await loadDaemonControl();
|
|
468
|
-
const { findClaudeTranscript, readClaudeTranscript } =
|
|
469
|
-
"./team/claude-transcript.mjs"
|
|
470
|
-
);
|
|
468
|
+
const { claudeWaitVerdict, findClaudeTranscript, readClaudeTranscript } =
|
|
469
|
+
await import("./team/claude-transcript.mjs");
|
|
471
470
|
const timeoutMs = numericOption(payload.timeoutMs, 30_000);
|
|
472
471
|
const pollIntervalMs = Math.max(
|
|
473
472
|
10,
|
|
@@ -535,28 +534,8 @@ async function cmdDaemonWait(args) {
|
|
|
535
534
|
target: probe.target,
|
|
536
535
|
...daemonProbeMetadata(probe),
|
|
537
536
|
};
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
...last,
|
|
541
|
-
ok: false,
|
|
542
|
-
status: "failed",
|
|
543
|
-
error: transcript.error,
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
if (
|
|
547
|
-
(idle || transcript?.sectionClosed) &&
|
|
548
|
-
transcript?.userSeen &&
|
|
549
|
-
transcript.turnEnded
|
|
550
|
-
) {
|
|
551
|
-
if (!transcript.response.trim())
|
|
552
|
-
return emitJson({
|
|
553
|
-
...last,
|
|
554
|
-
ok: false,
|
|
555
|
-
status: "failed",
|
|
556
|
-
error: "turn ended without assistant text",
|
|
557
|
-
});
|
|
558
|
-
return emitJson({ ...last, status: "completed", done: true });
|
|
559
|
-
}
|
|
537
|
+
const verdict = claudeWaitVerdict(transcript, idle);
|
|
538
|
+
if (verdict) return emitJson({ ...last, ...verdict });
|
|
560
539
|
if (Date.now() >= deadline) break;
|
|
561
540
|
await new Promise((resolve) =>
|
|
562
541
|
setTimeout(resolve, Math.min(pollIntervalMs, deadline - Date.now())),
|