triflux 3.1.0-dev.5 → 3.2.0-dev.10
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/README.ko.md +26 -18
- package/README.md +26 -18
- package/bin/triflux.mjs +1614 -1077
- package/hooks/hooks.json +12 -0
- package/hooks/keyword-rules.json +354 -0
- package/hub/bridge.mjs +371 -193
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/router.mjs +422 -161
- package/hub/server.mjs +429 -344
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +516 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +99 -0
- package/hub/team/dashboard.mjs +267 -0
- package/hub/team/native-supervisor.mjs +300 -0
- package/hub/team/native.mjs +62 -0
- package/hub/team/nativeProxy.mjs +534 -0
- package/hub/team/orchestrator.mjs +166 -0
- package/hub/team/pane.mjs +138 -0
- package/hub/team/psmux.mjs +297 -0
- package/hub/team/session.mjs +608 -0
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +140 -53
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +1789 -1732
- package/package.json +6 -2
- package/scripts/__tests__/keyword-detector.test.mjs +234 -0
- package/scripts/hub-ensure.mjs +83 -0
- package/scripts/keyword-detector.mjs +272 -0
- package/scripts/keyword-rules-expander.mjs +521 -0
- package/scripts/lib/keyword-rules.mjs +168 -0
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/run.cjs +62 -0
- package/scripts/setup.mjs +189 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +49 -0
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +943 -508
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +77 -0
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-multi/SKILL.md +296 -0
- package/skills/tfx-setup/SKILL.md +1 -4
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// hub/team/cli-team-status.mjs — team 상태/조회 로직
|
|
2
|
+
import {
|
|
3
|
+
capturePaneOutput,
|
|
4
|
+
detectMultiplexer,
|
|
5
|
+
getSessionAttachedCount,
|
|
6
|
+
hasWindowsTerminal,
|
|
7
|
+
hasWindowsTerminalSession,
|
|
8
|
+
listSessions,
|
|
9
|
+
} from "./session.mjs";
|
|
10
|
+
import { AMBER, BOLD, DIM, GRAY, GREEN, RED, RESET, WHITE } from "./shared.mjs";
|
|
11
|
+
import {
|
|
12
|
+
TEAM_PROFILE,
|
|
13
|
+
getDefaultHubUrl,
|
|
14
|
+
getHubInfo,
|
|
15
|
+
isNativeMode,
|
|
16
|
+
isTeamAlive,
|
|
17
|
+
isWtMode,
|
|
18
|
+
loadTeamState,
|
|
19
|
+
nativeGetStatus,
|
|
20
|
+
ok,
|
|
21
|
+
saveTeamState,
|
|
22
|
+
} from "./cli-team-common.mjs";
|
|
23
|
+
|
|
24
|
+
async function fetchHubTaskList(state) {
|
|
25
|
+
const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
|
|
26
|
+
const teamName = state?.native?.teamName || state?.sessionName || null;
|
|
27
|
+
if (!teamName) return [];
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(`${hubBase}/bridge/team/task-list`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({ team_name: teamName }),
|
|
33
|
+
signal: AbortSignal.timeout(2000),
|
|
34
|
+
});
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
return data?.ok ? (data.data?.tasks || []) : [];
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderTasks(tasks = []) {
|
|
43
|
+
if (!tasks.length) {
|
|
44
|
+
console.log(`\n ${DIM}태스크 없음${RESET}\n`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`\n ${AMBER}${BOLD}⬡ Team Tasks${RESET}\n`);
|
|
49
|
+
for (const t of tasks) {
|
|
50
|
+
const dep = t.depends_on?.length ? ` ${DIM}(deps: ${t.depends_on.join(",")})${RESET}` : "";
|
|
51
|
+
const owner = t.owner ? ` ${GRAY}[${t.owner}]${RESET}` : "";
|
|
52
|
+
console.log(` ${WHITE}${t.id}${RESET} ${t.status.padEnd(11)} ${t.title}${owner}${dep}`);
|
|
53
|
+
}
|
|
54
|
+
console.log("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function teamStatus() {
|
|
58
|
+
const state = loadTeamState();
|
|
59
|
+
if (!state) {
|
|
60
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const alive = isTeamAlive(state);
|
|
65
|
+
const status = alive ? `${GREEN}● active${RESET}` : `${RED}● dead${RESET}`;
|
|
66
|
+
const uptime = alive ? `${Math.round((Date.now() - state.startedAt) / 60000)}분` : "-";
|
|
67
|
+
|
|
68
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET} ${status}\n`);
|
|
69
|
+
console.log(` 세션: ${state.sessionName}`);
|
|
70
|
+
console.log(` 모드: ${state.teammateMode || "tmux"}`);
|
|
71
|
+
console.log(` 리드: ${state.lead || "claude"}`);
|
|
72
|
+
console.log(` 워커: ${(state.agents || []).join(", ")}`);
|
|
73
|
+
console.log(` Uptime: ${uptime}`);
|
|
74
|
+
console.log(` 태스크: ${(state.tasks || []).length}`);
|
|
75
|
+
if (isWtMode(state) && !hasWindowsTerminalSession()) {
|
|
76
|
+
console.log(` ${DIM}WT_SESSION 미감지: 생존성은 heuristics로 판정됨${RESET}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const members = state.members || [];
|
|
80
|
+
if (members.length) {
|
|
81
|
+
console.log("");
|
|
82
|
+
for (const m of members) {
|
|
83
|
+
const roleTag = m.role === "lead" ? "lead" : "worker";
|
|
84
|
+
console.log(` - ${m.name} (${m.cli}) ${DIM}${roleTag}${RESET} ${DIM}${m.pane}${RESET}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isNativeMode(state) && alive) {
|
|
89
|
+
const native = await nativeGetStatus(state);
|
|
90
|
+
const nativeMembers = native?.data?.members || [];
|
|
91
|
+
if (nativeMembers.length) {
|
|
92
|
+
console.log("");
|
|
93
|
+
for (const m of nativeMembers) {
|
|
94
|
+
console.log(` • ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (alive) {
|
|
100
|
+
const hubTasks = await fetchHubTaskList(state);
|
|
101
|
+
if (hubTasks.length > 0) {
|
|
102
|
+
const completed = hubTasks.filter((t) => t.status === "completed").length;
|
|
103
|
+
const failed = hubTasks.filter((t) => t.status === "failed").length;
|
|
104
|
+
|
|
105
|
+
console.log(`\n ${BOLD}Hub Tasks${RESET} ${DIM}(${completed}/${hubTasks.length} done)${RESET}`);
|
|
106
|
+
for (const t of hubTasks) {
|
|
107
|
+
const icon = t.status === "completed" ? `${GREEN}✓${RESET}`
|
|
108
|
+
: t.status === "in_progress" ? `${AMBER}●${RESET}`
|
|
109
|
+
: t.status === "failed" ? `${RED}✗${RESET}`
|
|
110
|
+
: `${GRAY}○${RESET}`;
|
|
111
|
+
const owner = t.owner ? ` ${GRAY}[${t.owner}]${RESET}` : "";
|
|
112
|
+
const subject = t.subject || t.description?.slice(0, 50) || "";
|
|
113
|
+
console.log(` ${icon} ${subject}${owner}`);
|
|
114
|
+
}
|
|
115
|
+
if (failed > 0) console.log(` ${RED}⚠ ${failed}건 실패${RESET}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log("");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function teamTasks() {
|
|
123
|
+
const state = loadTeamState();
|
|
124
|
+
if (!state || !isTeamAlive(state)) {
|
|
125
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
renderTasks(state.tasks || []);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function teamTaskUpdate() {
|
|
132
|
+
const state = loadTeamState();
|
|
133
|
+
if (!state || !isTeamAlive(state)) {
|
|
134
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const action = (process.argv[4] || "").toLowerCase();
|
|
139
|
+
const taskId = (process.argv[5] || "").toUpperCase();
|
|
140
|
+
const nextStatus = action === "done" || action === "complete" || action === "completed"
|
|
141
|
+
? "completed"
|
|
142
|
+
: action === "progress" || action === "in-progress" || action === "in_progress"
|
|
143
|
+
? "in_progress"
|
|
144
|
+
: action === "pending"
|
|
145
|
+
? "pending"
|
|
146
|
+
: null;
|
|
147
|
+
|
|
148
|
+
if (!nextStatus || !taskId) {
|
|
149
|
+
console.log(`\n 사용법: ${WHITE}tfx multi task <pending|progress|done> <T1>${RESET}\n`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const tasks = state.tasks || [];
|
|
154
|
+
const target = tasks.find((t) => String(t.id).toUpperCase() === taskId);
|
|
155
|
+
if (!target) {
|
|
156
|
+
console.log(`\n ${DIM}태스크를 찾을 수 없음: ${taskId}${RESET}\n`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
target.status = nextStatus;
|
|
161
|
+
saveTeamState(state);
|
|
162
|
+
ok(`${target.id} 상태 갱신: ${nextStatus}`);
|
|
163
|
+
console.log("");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function teamDebug() {
|
|
167
|
+
const state = loadTeamState();
|
|
168
|
+
const linesIdx = process.argv.findIndex((a) => a === "--lines" || a === "-n");
|
|
169
|
+
const lines = linesIdx !== -1 ? Math.max(3, parseInt(process.argv[linesIdx + 1] || "20", 10) || 20) : 20;
|
|
170
|
+
const mux = detectMultiplexer() || "none";
|
|
171
|
+
const hub = await getHubInfo();
|
|
172
|
+
|
|
173
|
+
console.log(`\n ${AMBER}${BOLD}⬡ Team Debug${RESET}\n`);
|
|
174
|
+
console.log(` platform: ${process.platform}`);
|
|
175
|
+
console.log(` node: ${process.version}`);
|
|
176
|
+
console.log(` tty: stdout=${!!process.stdout.isTTY}, stdin=${!!process.stdin.isTTY}`);
|
|
177
|
+
console.log(` mux: ${mux}`);
|
|
178
|
+
console.log(` hub-pid: ${hub ? `${hub.pid}` : "-"}`);
|
|
179
|
+
console.log(` hub-url: ${hub?.url || "-"}`);
|
|
180
|
+
|
|
181
|
+
const sessions = listSessions();
|
|
182
|
+
console.log(` sessions: ${sessions.length ? sessions.join(", ") : "-"}`);
|
|
183
|
+
|
|
184
|
+
if (!state) {
|
|
185
|
+
console.log(`\n ${DIM}team-state 없음 (활성 세션 없음)${RESET}\n`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log(`\n ${BOLD}state${RESET}`);
|
|
190
|
+
console.log(` session: ${state.sessionName}`);
|
|
191
|
+
console.log(` profile: ${state.profile || TEAM_PROFILE}`);
|
|
192
|
+
console.log(` mode: ${state.teammateMode || "tmux"}`);
|
|
193
|
+
console.log(` lead: ${state.lead}`);
|
|
194
|
+
console.log(` agents: ${(state.agents || []).join(", ")}`);
|
|
195
|
+
console.log(` alive: ${isTeamAlive(state) ? "yes" : "no"}`);
|
|
196
|
+
const attached = getSessionAttachedCount(state.sessionName);
|
|
197
|
+
console.log(` attached: ${attached == null ? "-" : attached}`);
|
|
198
|
+
|
|
199
|
+
if (isWtMode(state)) {
|
|
200
|
+
const wtState = state.wt || {};
|
|
201
|
+
console.log(`\n ${BOLD}wt-session${RESET}`);
|
|
202
|
+
console.log(` window: ${wtState.windowId ?? 0}`);
|
|
203
|
+
console.log(` layout: ${wtState.layout || state.layout || "-"}`);
|
|
204
|
+
console.log(` panes: ${wtState.paneCount ?? (state.members || []).length}`);
|
|
205
|
+
console.log(` wt.exe: ${hasWindowsTerminal() ? "yes" : "no"}`);
|
|
206
|
+
console.log(` WT_SESSION:${hasWindowsTerminalSession() ? "yes" : "no"}`);
|
|
207
|
+
console.log("");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (isNativeMode(state)) {
|
|
212
|
+
const native = await nativeGetStatus(state);
|
|
213
|
+
const members = native?.data?.members || [];
|
|
214
|
+
console.log(`\n ${BOLD}native-members${RESET}`);
|
|
215
|
+
if (!members.length) {
|
|
216
|
+
console.log(` ${DIM}(no data)${RESET}`);
|
|
217
|
+
} else {
|
|
218
|
+
for (const m of members) {
|
|
219
|
+
console.log(` - ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
console.log("");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const members = state.members || [];
|
|
227
|
+
console.log(`\n ${BOLD}pane-tail${RESET} ${DIM}(last ${lines} lines)${RESET}`);
|
|
228
|
+
if (!members.length) {
|
|
229
|
+
console.log(` ${DIM}(members 없음)${RESET}`);
|
|
230
|
+
} else {
|
|
231
|
+
for (const m of members) {
|
|
232
|
+
const tail = capturePaneOutput(m.pane, lines) || "(empty)";
|
|
233
|
+
console.log(`\n [${m.name}] ${m.pane}`);
|
|
234
|
+
const tailLines = tail.split("\n").slice(-lines);
|
|
235
|
+
for (const line of tailLines) {
|
|
236
|
+
console.log(` ${line}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
console.log("");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function teamList() {
|
|
244
|
+
const state = loadTeamState();
|
|
245
|
+
if (state && isNativeMode(state) && isTeamAlive(state)) {
|
|
246
|
+
console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
|
|
247
|
+
console.log(` ${GREEN}●${RESET} ${state.sessionName} ${DIM}(in-process)${RESET}`);
|
|
248
|
+
console.log("");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (state && isWtMode(state) && isTeamAlive(state)) {
|
|
252
|
+
console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
|
|
253
|
+
console.log(` ${GREEN}●${RESET} ${state.sessionName} ${DIM}(wt)${RESET}`);
|
|
254
|
+
console.log("");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const sessions = listSessions();
|
|
259
|
+
if (sessions.length === 0) {
|
|
260
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
|
|
264
|
+
for (const sessionName of sessions) {
|
|
265
|
+
console.log(` ${GREEN}●${RESET} ${sessionName}`);
|
|
266
|
+
}
|
|
267
|
+
console.log("");
|
|
268
|
+
}
|
|
269
|
+
|
package/hub/team/cli.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// hub/team/cli.mjs — team CLI UI/네비게이션 진입점
|
|
2
|
+
// bin/triflux.mjs에서 import하여 사용
|
|
3
|
+
import { AMBER, GRAY, DIM, BOLD, RESET, WHITE } from "./shared.mjs";
|
|
4
|
+
import { TEAM_SUBCOMMANDS } from "./cli-team-common.mjs";
|
|
5
|
+
import { teamStart } from "./cli-team-start.mjs";
|
|
6
|
+
import { teamStatus, teamTasks, teamTaskUpdate, teamDebug, teamList } from "./cli-team-status.mjs";
|
|
7
|
+
import {
|
|
8
|
+
teamAttach,
|
|
9
|
+
teamFocus,
|
|
10
|
+
teamInterrupt,
|
|
11
|
+
teamControl,
|
|
12
|
+
teamStop,
|
|
13
|
+
teamKill,
|
|
14
|
+
teamSend,
|
|
15
|
+
} from "./cli-team-control.mjs";
|
|
16
|
+
|
|
17
|
+
function teamHelp() {
|
|
18
|
+
console.log(`
|
|
19
|
+
${AMBER}${BOLD}⬡ tfx multi${RESET} ${DIM}멀티-CLI 팀 모드 (Lead + Teammates)${RESET}
|
|
20
|
+
|
|
21
|
+
${BOLD}시작${RESET}
|
|
22
|
+
${WHITE}tfx multi "작업 설명"${RESET}
|
|
23
|
+
${WHITE}tfx multi --agents codex,gemini --lead claude "작업"${RESET}
|
|
24
|
+
${WHITE}tfx multi --teammate-mode tmux "작업"${RESET}
|
|
25
|
+
${WHITE}tfx multi --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}
|
|
26
|
+
${WHITE}tfx multi --layout 1xN "작업"${RESET} ${DIM}(세로 분할 컬럼)${RESET}
|
|
27
|
+
${WHITE}tfx multi --layout Nx1 "작업"${RESET} ${DIM}(가로 분할 스택)${RESET}
|
|
28
|
+
${WHITE}tfx multi --teammate-mode in-process "작업"${RESET} ${DIM}(tmux 불필요)${RESET}
|
|
29
|
+
|
|
30
|
+
${BOLD}제어${RESET}
|
|
31
|
+
${WHITE}tfx multi status${RESET} ${GRAY}현재 팀 상태${RESET}
|
|
32
|
+
${WHITE}tfx multi debug${RESET} ${DIM}[--lines 30]${RESET} ${GRAY}강화 디버그 출력(환경/세션/pane tail)${RESET}
|
|
33
|
+
${WHITE}tfx multi tasks${RESET} ${GRAY}공유 태스크 목록${RESET}
|
|
34
|
+
${WHITE}tfx multi task${RESET} ${DIM}<pending|progress|done> <T1>${RESET} ${GRAY}태스크 상태 갱신${RESET}
|
|
35
|
+
${WHITE}tfx multi attach${RESET} ${DIM}[--wt]${RESET} ${GRAY}세션 재연결 (WT 분할은 opt-in)${RESET}
|
|
36
|
+
${WHITE}tfx multi focus${RESET} ${DIM}<lead|이름|번호> [--wt]${RESET} ${GRAY}특정 팀메이트 포커스${RESET}
|
|
37
|
+
${WHITE}tfx multi send${RESET} ${DIM}<lead|이름|번호> "msg"${RESET} ${GRAY}팀메이트에 메시지 주입${RESET}
|
|
38
|
+
${WHITE}tfx multi interrupt${RESET} ${DIM}<대상>${RESET} ${GRAY}팀메이트 인터럽트(C-c)${RESET}
|
|
39
|
+
${WHITE}tfx multi control${RESET} ${DIM}<대상> <cmd>${RESET} ${GRAY}리드 제어명령(interrupt|stop|pause|resume)${RESET}
|
|
40
|
+
${WHITE}tfx multi stop${RESET} ${GRAY}graceful 종료${RESET}
|
|
41
|
+
${WHITE}tfx multi kill${RESET} ${GRAY}모든 팀 세션 강제 종료${RESET}
|
|
42
|
+
${WHITE}tfx multi list${RESET} ${GRAY}활성 세션 목록${RESET}
|
|
43
|
+
|
|
44
|
+
${BOLD}키 조작(Claude teammate 스타일, tmux 모드)${RESET}
|
|
45
|
+
${WHITE}Shift+Down${RESET} ${GRAY}다음 팀메이트${RESET}
|
|
46
|
+
${WHITE}Shift+Tab${RESET} ${GRAY}이전 팀메이트 (권장)${RESET}
|
|
47
|
+
${WHITE}Shift+Left${RESET} ${GRAY}이전 팀메이트 (대체)${RESET}
|
|
48
|
+
${WHITE}Shift+Up${RESET} ${GRAY}미지원 (Claude Code가 캡처 불가, scroll-up 충돌)${RESET}
|
|
49
|
+
${WHITE}Escape${RESET} ${GRAY}현재 팀메이트 인터럽트${RESET}
|
|
50
|
+
${WHITE}Ctrl+T${RESET} ${GRAY}태스크 목록 토글${RESET}
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* tfx multi 서브커맨드 라우터
|
|
56
|
+
* bin/triflux.mjs에서 호출
|
|
57
|
+
*/
|
|
58
|
+
export async function cmdTeam() {
|
|
59
|
+
const rawSub = process.argv[3];
|
|
60
|
+
const sub = typeof rawSub === "string" ? rawSub.toLowerCase() : rawSub;
|
|
61
|
+
|
|
62
|
+
switch (sub) {
|
|
63
|
+
case "status":
|
|
64
|
+
return teamStatus();
|
|
65
|
+
case "debug":
|
|
66
|
+
return teamDebug();
|
|
67
|
+
case "tasks":
|
|
68
|
+
return teamTasks();
|
|
69
|
+
case "task":
|
|
70
|
+
return teamTaskUpdate();
|
|
71
|
+
case "attach":
|
|
72
|
+
return teamAttach();
|
|
73
|
+
case "focus":
|
|
74
|
+
return teamFocus();
|
|
75
|
+
case "interrupt":
|
|
76
|
+
return teamInterrupt();
|
|
77
|
+
case "control":
|
|
78
|
+
return teamControl();
|
|
79
|
+
case "stop":
|
|
80
|
+
return teamStop();
|
|
81
|
+
case "kill":
|
|
82
|
+
return teamKill();
|
|
83
|
+
case "send":
|
|
84
|
+
return teamSend();
|
|
85
|
+
case "list":
|
|
86
|
+
return teamList();
|
|
87
|
+
case "help":
|
|
88
|
+
case "--help":
|
|
89
|
+
case "-h":
|
|
90
|
+
return teamHelp();
|
|
91
|
+
case undefined:
|
|
92
|
+
return teamHelp();
|
|
93
|
+
default:
|
|
94
|
+
if (typeof sub === "string" && !sub.startsWith("-") && TEAM_SUBCOMMANDS.has(sub)) {
|
|
95
|
+
return teamHelp();
|
|
96
|
+
}
|
|
97
|
+
return teamStart();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hub/team/dashboard.mjs — 실시간 팀 상태 표시 (v2.2)
|
|
3
|
+
// tmux 의존 제거 — Hub task-list + native-supervisor 기반
|
|
4
|
+
//
|
|
5
|
+
// 실행:
|
|
6
|
+
// node hub/team/dashboard.mjs --session <세션이름> [--interval 2]
|
|
7
|
+
// node hub/team/dashboard.mjs --team <팀이름> [--interval 2]
|
|
8
|
+
import { get } from "node:http";
|
|
9
|
+
import { AMBER, GREEN, RED, GRAY, DIM, BOLD, RESET } from "./shared.mjs";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* HTTP GET JSON
|
|
13
|
+
* @param {string} url
|
|
14
|
+
* @returns {Promise<object|null>}
|
|
15
|
+
*/
|
|
16
|
+
function fetchJson(url) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const req = get(url, { timeout: 2000 }, (res) => {
|
|
19
|
+
let data = "";
|
|
20
|
+
res.on("data", (chunk) => (data += chunk));
|
|
21
|
+
res.on("end", () => {
|
|
22
|
+
try { resolve(JSON.parse(data)); } catch { resolve(null); }
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
req.on("error", () => resolve(null));
|
|
26
|
+
req.on("timeout", () => { req.destroy(); resolve(null); });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* HTTP POST JSON (Hub bridge 용)
|
|
32
|
+
* @param {string} url
|
|
33
|
+
* @param {object} body
|
|
34
|
+
* @returns {Promise<object|null>}
|
|
35
|
+
*/
|
|
36
|
+
async function fetchPost(url, body = {}) {
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(url, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "Content-Type": "application/json" },
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
signal: AbortSignal.timeout(2000),
|
|
43
|
+
});
|
|
44
|
+
return await res.json();
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 진행률 바 생성
|
|
52
|
+
* @param {number} pct — 0~100
|
|
53
|
+
* @param {number} width — 바 너비 (기본 8)
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function progressBar(pct, width = 8) {
|
|
57
|
+
const filled = Math.round((pct / 100) * width);
|
|
58
|
+
const empty = width - filled;
|
|
59
|
+
return `${GREEN}${"█".repeat(filled)}${GRAY}${"░".repeat(empty)}${RESET}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 업타임 포맷
|
|
64
|
+
* @param {number} ms
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
function formatUptime(ms) {
|
|
68
|
+
if (ms < 60000) return `${Math.round(ms / 1000)}초`;
|
|
69
|
+
if (ms < 3600000) return `${Math.round(ms / 60000)}분`;
|
|
70
|
+
return `${Math.round(ms / 3600000)}시간`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* task 상태 아이콘
|
|
75
|
+
* @param {string} status
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
function statusIcon(status) {
|
|
79
|
+
switch (status) {
|
|
80
|
+
case "completed": return `${GREEN}✓${RESET}`;
|
|
81
|
+
case "in_progress": return `${AMBER}●${RESET}`;
|
|
82
|
+
case "failed": return `${RED}✗${RESET}`;
|
|
83
|
+
default: return `${GRAY}○${RESET}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 멤버 목록 구성: Hub tasks + supervisor + teamState 통합
|
|
89
|
+
* @param {Array} hubTasks — Hub bridge task-list 결과
|
|
90
|
+
* @param {Array} supervisorMembers — native-supervisor 멤버 상태
|
|
91
|
+
* @param {object} teamState — team-state.json 내용
|
|
92
|
+
* @returns {Array<{name: string, cli: string, status: string, subject: string, preview: string}>}
|
|
93
|
+
*/
|
|
94
|
+
function buildMemberList(hubTasks, supervisorMembers, teamState) {
|
|
95
|
+
const members = [];
|
|
96
|
+
const supervisorByName = new Map(supervisorMembers.map((m) => [m.name, m]));
|
|
97
|
+
|
|
98
|
+
// Hub tasks가 있으면 주 데이터 소스
|
|
99
|
+
if (hubTasks.length > 0) {
|
|
100
|
+
for (const task of hubTasks) {
|
|
101
|
+
const owner = task.owner || task.subject || "";
|
|
102
|
+
const sup = supervisorByName.get(owner);
|
|
103
|
+
members.push({
|
|
104
|
+
name: owner,
|
|
105
|
+
cli: task.metadata?.cli || sup?.cli || "",
|
|
106
|
+
status: task.status || "pending",
|
|
107
|
+
subject: task.subject || "",
|
|
108
|
+
preview: sup?.lastPreview || task.description?.slice(0, 80) || "",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return members;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Supervisor 데이터 폴백
|
|
115
|
+
if (supervisorMembers.length > 0) {
|
|
116
|
+
for (const m of supervisorMembers) {
|
|
117
|
+
if (m.role === "lead") continue;
|
|
118
|
+
members.push({
|
|
119
|
+
name: m.name,
|
|
120
|
+
cli: m.cli || "",
|
|
121
|
+
status: m.status === "running" ? "in_progress" : m.status === "exited" ? "completed" : m.status,
|
|
122
|
+
subject: "",
|
|
123
|
+
preview: m.lastPreview || "",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return members;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// teamState 폴백 (하위 호환)
|
|
130
|
+
const panes = teamState?.panes || {};
|
|
131
|
+
for (const [, paneInfo] of Object.entries(panes).filter(([, v]) => v.role !== "dashboard" && v.role !== "lead")) {
|
|
132
|
+
members.push({
|
|
133
|
+
name: paneInfo.agentId || paneInfo.name || "?",
|
|
134
|
+
cli: paneInfo.cli || "",
|
|
135
|
+
status: "unknown",
|
|
136
|
+
subject: paneInfo.subtask || "",
|
|
137
|
+
preview: "",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return members;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 대시보드 렌더링 (v2.2: Hub/supervisor 기반)
|
|
145
|
+
* @param {string} sessionName — 세션 또는 팀 이름
|
|
146
|
+
* @param {object} opts
|
|
147
|
+
* @param {string} opts.hubUrl — Hub URL (기본 http://127.0.0.1:27888)
|
|
148
|
+
* @param {string} [opts.teamName] — Hub task-list 조회용 팀 이름
|
|
149
|
+
* @param {string} [opts.supervisorUrl] — native-supervisor 제어 URL
|
|
150
|
+
* @param {object} [opts.teamState] — team-state.json 내용 (하위 호환)
|
|
151
|
+
*/
|
|
152
|
+
export async function renderDashboard(sessionName, opts = {}) {
|
|
153
|
+
const {
|
|
154
|
+
hubUrl = "http://127.0.0.1:27888",
|
|
155
|
+
teamName,
|
|
156
|
+
supervisorUrl,
|
|
157
|
+
teamState = {},
|
|
158
|
+
} = opts;
|
|
159
|
+
const W = 50;
|
|
160
|
+
const border = "─".repeat(W);
|
|
161
|
+
|
|
162
|
+
// 데이터 수집 (병렬)
|
|
163
|
+
const [hubStatus, taskListRes, supervisorRes] = await Promise.all([
|
|
164
|
+
fetchJson(`${hubUrl}/status`),
|
|
165
|
+
teamName ? fetchPost(`${hubUrl}/bridge/team/task-list`, { team_name: teamName }) : null,
|
|
166
|
+
supervisorUrl ? fetchJson(`${supervisorUrl}/status`) : null,
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const hubOnline = !!hubStatus;
|
|
170
|
+
const hubState = hubOnline ? `${GREEN}● online${RESET}` : `${RED}● offline${RESET}`;
|
|
171
|
+
const uptime = hubStatus?.hub?.uptime ? formatUptime(hubStatus.hub.uptime) : "-";
|
|
172
|
+
const queueSize = hubStatus?.hub?.queue_depth ?? 0;
|
|
173
|
+
|
|
174
|
+
// Hub task 데이터
|
|
175
|
+
const hubTasks = taskListRes?.ok ? (taskListRes.data?.tasks || []) : [];
|
|
176
|
+
const completedCount = hubTasks.filter((t) => t.status === "completed").length;
|
|
177
|
+
const totalCount = hubTasks.length;
|
|
178
|
+
|
|
179
|
+
// Supervisor 멤버 데이터
|
|
180
|
+
const supervisorMembers = supervisorRes?.ok ? (supervisorRes.data?.members || []) : [];
|
|
181
|
+
|
|
182
|
+
// 헤더
|
|
183
|
+
const progress = totalCount > 0 ? ` ${completedCount}/${totalCount}` : "";
|
|
184
|
+
console.log(`${AMBER}┌─ ${sessionName}${progress} ${GRAY}${"─".repeat(Math.max(0, W - sessionName.length - progress.length - 3))}${AMBER}┐${RESET}`);
|
|
185
|
+
console.log(`${AMBER}│${RESET} Hub: ${hubState} Uptime: ${DIM}${uptime}${RESET} Queue: ${DIM}${queueSize}${RESET}`);
|
|
186
|
+
console.log(`${AMBER}│${RESET}`);
|
|
187
|
+
|
|
188
|
+
// 멤버/워커 렌더링
|
|
189
|
+
const members = buildMemberList(hubTasks, supervisorMembers, teamState);
|
|
190
|
+
|
|
191
|
+
if (members.length === 0) {
|
|
192
|
+
console.log(`${AMBER}│${RESET} ${DIM}에이전트 정보 없음${RESET}`);
|
|
193
|
+
} else {
|
|
194
|
+
for (const m of members) {
|
|
195
|
+
const icon = statusIcon(m.status);
|
|
196
|
+
const label = `[${m.name}]`;
|
|
197
|
+
const cliTag = m.cli ? m.cli.charAt(0).toUpperCase() + m.cli.slice(1) : "";
|
|
198
|
+
|
|
199
|
+
// 진행률 추정
|
|
200
|
+
const pct = m.status === "completed" ? 100
|
|
201
|
+
: m.status === "in_progress" ? 50
|
|
202
|
+
: m.status === "failed" ? 100
|
|
203
|
+
: 0;
|
|
204
|
+
|
|
205
|
+
console.log(`${AMBER}│${RESET} ${BOLD}${label}${RESET} ${cliTag} ${icon} ${m.status || "pending"} ${progressBar(pct)}`);
|
|
206
|
+
|
|
207
|
+
// 미리보기: supervisor lastPreview > task subject
|
|
208
|
+
const preview = m.preview || m.subject || "";
|
|
209
|
+
if (preview) {
|
|
210
|
+
const truncated = preview.length > W - 8 ? preview.slice(0, W - 11) + "..." : preview;
|
|
211
|
+
console.log(`${AMBER}│${RESET} ${DIM}> ${truncated}${RESET}`);
|
|
212
|
+
}
|
|
213
|
+
console.log(`${AMBER}│${RESET}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 푸터
|
|
218
|
+
console.log(`${AMBER}└${GRAY}${border}${AMBER}┘${RESET}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** team-state.json 로드 */
|
|
222
|
+
async function loadTeamState() {
|
|
223
|
+
try {
|
|
224
|
+
const { readFileSync } = await import("node:fs");
|
|
225
|
+
const { join } = await import("node:path");
|
|
226
|
+
const { homedir } = await import("node:os");
|
|
227
|
+
const statePath = join(homedir(), ".claude", "cache", "tfx-hub", "team-state.json");
|
|
228
|
+
return JSON.parse(readFileSync(statePath, "utf8"));
|
|
229
|
+
} catch {
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── CLI 실행 ──
|
|
235
|
+
if (process.argv[1]?.includes("dashboard.mjs")) {
|
|
236
|
+
const sessionIdx = process.argv.indexOf("--session");
|
|
237
|
+
const teamIdx = process.argv.indexOf("--team");
|
|
238
|
+
const sessionName = sessionIdx !== -1 ? process.argv[sessionIdx + 1] : null;
|
|
239
|
+
const teamName = teamIdx !== -1 ? process.argv[teamIdx + 1] : null;
|
|
240
|
+
const intervalSec = parseInt(process.argv[process.argv.indexOf("--interval") + 1] || "2", 10);
|
|
241
|
+
|
|
242
|
+
const displayName = sessionName || teamName;
|
|
243
|
+
if (!displayName) {
|
|
244
|
+
console.error("사용법: node dashboard.mjs --session <세션이름> [--team <팀이름>] [--interval 2]");
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Ctrl+C로 종료
|
|
249
|
+
process.on("SIGINT", () => process.exit(0));
|
|
250
|
+
|
|
251
|
+
// 갱신 루프
|
|
252
|
+
while (true) {
|
|
253
|
+
const teamState = await loadTeamState();
|
|
254
|
+
const effectiveTeamName = teamName || null;
|
|
255
|
+
const supervisorUrl = teamState?.native?.controlUrl || null;
|
|
256
|
+
|
|
257
|
+
// 화면 클리어 (ANSI)
|
|
258
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
259
|
+
await renderDashboard(displayName, {
|
|
260
|
+
teamName: effectiveTeamName,
|
|
261
|
+
supervisorUrl,
|
|
262
|
+
teamState,
|
|
263
|
+
});
|
|
264
|
+
console.log(`${DIM} ${intervalSec}초 간격 갱신 | Ctrl+C로 종료${RESET}`);
|
|
265
|
+
await new Promise((r) => setTimeout(r, intervalSec * 1000));
|
|
266
|
+
}
|
|
267
|
+
}
|