triflux 3.2.0-dev.3 → 3.2.0-dev.6

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,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // hub/team/dashboard.mjs — 실시간 팀 상태 표시
3
- // 실행: watch -n 1 -c 'node hub/team/dashboard.mjs --session tfx-team-xxx'
4
- // 또는: node hub/team/dashboard.mjs --session tfx-team-xxx (단일 출력)
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]
5
8
  import { get } from "node:http";
6
- import { capturePaneOutput } from "./session.mjs";
7
9
 
8
10
  // ── 색상 ──
9
11
  const AMBER = "\x1b[38;5;214m";
@@ -15,32 +17,44 @@ const BOLD = "\x1b[1m";
15
17
  const RESET = "\x1b[0m";
16
18
 
17
19
  /**
18
- * Hub /status 엔드포인트 조회
19
- * @param {string} hubUrl — 예: http://127.0.0.1:27888
20
+ * HTTP GET JSON
21
+ * @param {string} url
20
22
  * @returns {Promise<object|null>}
21
23
  */
22
- function fetchStatus(hubUrl) {
24
+ function fetchJson(url) {
23
25
  return new Promise((resolve) => {
24
- const url = `${hubUrl}/status`;
25
26
  const req = get(url, { timeout: 2000 }, (res) => {
26
27
  let data = "";
27
28
  res.on("data", (chunk) => (data += chunk));
28
29
  res.on("end", () => {
29
- try {
30
- resolve(JSON.parse(data));
31
- } catch {
32
- resolve(null);
33
- }
30
+ try { resolve(JSON.parse(data)); } catch { resolve(null); }
34
31
  });
35
32
  });
36
33
  req.on("error", () => resolve(null));
37
- req.on("timeout", () => {
38
- req.destroy();
39
- resolve(null);
40
- });
34
+ req.on("timeout", () => { req.destroy(); resolve(null); });
41
35
  });
42
36
  }
43
37
 
38
+ /**
39
+ * HTTP POST JSON (Hub bridge 용)
40
+ * @param {string} url
41
+ * @param {object} body
42
+ * @returns {Promise<object|null>}
43
+ */
44
+ async function fetchPost(url, body = {}) {
45
+ try {
46
+ const res = await fetch(url, {
47
+ method: "POST",
48
+ headers: { "Content-Type": "application/json" },
49
+ body: JSON.stringify(body),
50
+ signal: AbortSignal.timeout(2000),
51
+ });
52
+ return await res.json();
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
44
58
  /**
45
59
  * 진행률 바 생성
46
60
  * @param {number} pct — 0~100
@@ -65,56 +79,141 @@ function formatUptime(ms) {
65
79
  }
66
80
 
67
81
  /**
68
- * 대시보드 렌더링
69
- * @param {string} sessionName — tmux 세션 이름
82
+ * task 상태 아이콘
83
+ * @param {string} status
84
+ * @returns {string}
85
+ */
86
+ function statusIcon(status) {
87
+ switch (status) {
88
+ case "completed": return `${GREEN}✓${RESET}`;
89
+ case "in_progress": return `${AMBER}●${RESET}`;
90
+ case "failed": return `${RED}✗${RESET}`;
91
+ default: return `${GRAY}○${RESET}`;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 멤버 목록 구성: Hub tasks + supervisor + teamState 통합
97
+ * @param {Array} hubTasks — Hub bridge task-list 결과
98
+ * @param {Array} supervisorMembers — native-supervisor 멤버 상태
99
+ * @param {object} teamState — team-state.json 내용
100
+ * @returns {Array<{name: string, cli: string, status: string, subject: string, preview: string}>}
101
+ */
102
+ function buildMemberList(hubTasks, supervisorMembers, teamState) {
103
+ const members = [];
104
+ const supervisorByName = new Map(supervisorMembers.map((m) => [m.name, m]));
105
+
106
+ // Hub tasks가 있으면 주 데이터 소스
107
+ if (hubTasks.length > 0) {
108
+ for (const task of hubTasks) {
109
+ const owner = task.owner || task.subject || "";
110
+ const sup = supervisorByName.get(owner);
111
+ members.push({
112
+ name: owner,
113
+ cli: task.metadata?.cli || sup?.cli || "",
114
+ status: task.status || "pending",
115
+ subject: task.subject || "",
116
+ preview: sup?.lastPreview || task.description?.slice(0, 80) || "",
117
+ });
118
+ }
119
+ return members;
120
+ }
121
+
122
+ // Supervisor 데이터 폴백
123
+ if (supervisorMembers.length > 0) {
124
+ for (const m of supervisorMembers) {
125
+ if (m.role === "lead") continue;
126
+ members.push({
127
+ name: m.name,
128
+ cli: m.cli || "",
129
+ status: m.status === "running" ? "in_progress" : m.status === "exited" ? "completed" : m.status,
130
+ subject: "",
131
+ preview: m.lastPreview || "",
132
+ });
133
+ }
134
+ return members;
135
+ }
136
+
137
+ // teamState 폴백 (하위 호환)
138
+ const panes = teamState?.panes || {};
139
+ for (const [, paneInfo] of Object.entries(panes).filter(([, v]) => v.role !== "dashboard" && v.role !== "lead")) {
140
+ members.push({
141
+ name: paneInfo.agentId || paneInfo.name || "?",
142
+ cli: paneInfo.cli || "",
143
+ status: "unknown",
144
+ subject: paneInfo.subtask || "",
145
+ preview: "",
146
+ });
147
+ }
148
+ return members;
149
+ }
150
+
151
+ /**
152
+ * 대시보드 렌더링 (v2.2: Hub/supervisor 기반)
153
+ * @param {string} sessionName — 세션 또는 팀 이름
70
154
  * @param {object} opts
71
155
  * @param {string} opts.hubUrl — Hub URL (기본 http://127.0.0.1:27888)
72
- * @param {object} opts.teamStateteam-state.json 내용
156
+ * @param {string} [opts.teamName]Hub task-list 조회용 팀 이름
157
+ * @param {string} [opts.supervisorUrl] — native-supervisor 제어 URL
158
+ * @param {object} [opts.teamState] — team-state.json 내용 (하위 호환)
73
159
  */
74
160
  export async function renderDashboard(sessionName, opts = {}) {
75
- const { hubUrl = "http://127.0.0.1:27888", teamState = {} } = opts;
161
+ const {
162
+ hubUrl = "http://127.0.0.1:27888",
163
+ teamName,
164
+ supervisorUrl,
165
+ teamState = {},
166
+ } = opts;
76
167
  const W = 50;
77
168
  const border = "─".repeat(W);
78
169
 
79
- // Hub 상태 조회
80
- const status = await fetchStatus(hubUrl);
81
- const hubOnline = !!status;
170
+ // 데이터 수집 (병렬)
171
+ const [hubStatus, taskListRes, supervisorRes] = await Promise.all([
172
+ fetchJson(`${hubUrl}/status`),
173
+ teamName ? fetchPost(`${hubUrl}/bridge/team/task-list`, { team_name: teamName }) : null,
174
+ supervisorUrl ? fetchJson(`${supervisorUrl}/status`) : null,
175
+ ]);
176
+
177
+ const hubOnline = !!hubStatus;
82
178
  const hubState = hubOnline ? `${GREEN}● online${RESET}` : `${RED}● offline${RESET}`;
83
- const uptime = status?.hub?.uptime ? formatUptime(status.hub.uptime) : "-";
84
- const queueSize = status?.hub?.queue_depth ?? 0;
179
+ const uptime = hubStatus?.hub?.uptime ? formatUptime(hubStatus.hub.uptime) : "-";
180
+ const queueSize = hubStatus?.hub?.queue_depth ?? 0;
181
+
182
+ // Hub task 데이터
183
+ const hubTasks = taskListRes?.ok ? (taskListRes.data?.tasks || []) : [];
184
+ const completedCount = hubTasks.filter((t) => t.status === "completed").length;
185
+ const totalCount = hubTasks.length;
186
+
187
+ // Supervisor 멤버 데이터
188
+ const supervisorMembers = supervisorRes?.ok ? (supervisorRes.data?.members || []) : [];
85
189
 
86
190
  // 헤더
87
- console.log(`${AMBER}┌─ ${sessionName} ${GRAY}${"─".repeat(Math.max(0, W - sessionName.length - 3))}${AMBER}┐${RESET}`);
191
+ const progress = totalCount > 0 ? ` ${completedCount}/${totalCount}` : "";
192
+ console.log(`${AMBER}┌─ ${sessionName}${progress} ${GRAY}${"─".repeat(Math.max(0, W - sessionName.length - progress.length - 3))}${AMBER}┐${RESET}`);
88
193
  console.log(`${AMBER}│${RESET} Hub: ${hubState} Uptime: ${DIM}${uptime}${RESET} Queue: ${DIM}${queueSize}${RESET}`);
89
194
  console.log(`${AMBER}│${RESET}`);
90
195
 
91
- // 에이전트 상태
92
- const panes = teamState?.panes || {};
93
- const paneEntries = Object.entries(panes).filter(([, v]) => v.role !== "dashboard");
196
+ // 멤버/워커 렌더링
197
+ const members = buildMemberList(hubTasks, supervisorMembers, teamState);
94
198
 
95
- if (paneEntries.length === 0) {
199
+ if (members.length === 0) {
96
200
  console.log(`${AMBER}│${RESET} ${DIM}에이전트 정보 없음${RESET}`);
97
201
  } else {
98
- for (const [paneTarget, paneInfo] of paneEntries) {
99
- const { cli = "?", agentId = "?", subtask = "" } = paneInfo;
100
- const label = `[${agentId}]`;
101
- const cliTag = cli.charAt(0).toUpperCase() + cli.slice(1);
102
-
103
- // Hub에서 에이전트 상태 확인
104
- const agentStatus = status?.agents?.find?.((a) => a.agent_id === agentId);
105
- const online = agentStatus ? `${GREEN}● online${RESET}` : `${GRAY}○ -${RESET}`;
106
-
107
- // 진행률 추정 (메시지 기반, 단순 휴리스틱)
108
- const msgCount = agentStatus?.message_count ?? 0;
109
- const pct = msgCount === 0 ? 0 : Math.min(100, msgCount * 25);
110
-
111
- console.log(`${AMBER}│${RESET} ${BOLD}${label}${RESET} ${cliTag} ${online} ${progressBar(pct)} ${DIM}${pct}%${RESET}`);
112
-
113
- // pane 미리보기 (마지막 2줄)
114
- const preview = capturePaneOutput(paneTarget, 2)
115
- .split("\n")
116
- .filter(Boolean)
117
- .slice(-1)[0] || "";
202
+ for (const m of members) {
203
+ const icon = statusIcon(m.status);
204
+ const label = `[${m.name}]`;
205
+ const cliTag = m.cli ? m.cli.charAt(0).toUpperCase() + m.cli.slice(1) : "";
206
+
207
+ // 진행률 추정
208
+ const pct = m.status === "completed" ? 100
209
+ : m.status === "in_progress" ? 50
210
+ : m.status === "failed" ? 100
211
+ : 0;
212
+
213
+ console.log(`${AMBER}│${RESET} ${BOLD}${label}${RESET} ${cliTag} ${icon} ${m.status || "pending"} ${progressBar(pct)}`);
214
+
215
+ // 미리보기: supervisor lastPreview > task subject
216
+ const preview = m.preview || m.subject || "";
118
217
  if (preview) {
119
218
  const truncated = preview.length > W - 8 ? preview.slice(0, W - 11) + "..." : preview;
120
219
  console.log(`${AMBER}│${RESET} ${DIM}> ${truncated}${RESET}`);
@@ -140,14 +239,17 @@ async function loadTeamState() {
140
239
  }
141
240
  }
142
241
 
143
- // ── CLI 실행 (자체 갱신 루프 — watch 불필요) ──
242
+ // ── CLI 실행 ──
144
243
  if (process.argv[1]?.includes("dashboard.mjs")) {
145
244
  const sessionIdx = process.argv.indexOf("--session");
245
+ const teamIdx = process.argv.indexOf("--team");
146
246
  const sessionName = sessionIdx !== -1 ? process.argv[sessionIdx + 1] : null;
247
+ const teamName = teamIdx !== -1 ? process.argv[teamIdx + 1] : null;
147
248
  const intervalSec = parseInt(process.argv[process.argv.indexOf("--interval") + 1] || "2", 10);
148
249
 
149
- if (!sessionName) {
150
- console.error("사용법: node dashboard.mjs --session <세션이름> [--interval 2]");
250
+ const displayName = sessionName || teamName;
251
+ if (!displayName) {
252
+ console.error("사용법: node dashboard.mjs --session <세션이름> [--team <팀이름>] [--interval 2]");
151
253
  process.exit(1);
152
254
  }
153
255
 
@@ -157,9 +259,16 @@ if (process.argv[1]?.includes("dashboard.mjs")) {
157
259
  // 갱신 루프
158
260
  while (true) {
159
261
  const teamState = await loadTeamState();
262
+ const effectiveTeamName = teamName || null;
263
+ const supervisorUrl = teamState?.native?.controlUrl || null;
264
+
160
265
  // 화면 클리어 (ANSI)
161
266
  process.stdout.write("\x1b[2J\x1b[H");
162
- await renderDashboard(sessionName, { teamState });
267
+ await renderDashboard(displayName, {
268
+ teamName: effectiveTeamName,
269
+ supervisorUrl,
270
+ teamState,
271
+ });
163
272
  console.log(`${DIM} ${intervalSec}초 간격 갱신 | Ctrl+C로 종료${RESET}`);
164
273
  await new Promise((r) => setTimeout(r, intervalSec * 1000));
165
274
  }
@@ -83,6 +83,44 @@ export function buildTeamConfig(teamName, assignments) {
83
83
  };
84
84
  }
85
85
 
86
+ /**
87
+ * v2.2 슬림 래퍼 프롬프트 생성
88
+ * Agent spawn으로 네비게이션에 등록하되, 실제 작업은 tfx-route.sh가 수행.
89
+ * 프롬프트 ~100 토큰 목표 (v2의 ~500 대비 80% 감소).
90
+ *
91
+ * @param {'codex'|'gemini'} cli — CLI 타입
92
+ * @param {object} opts
93
+ * @param {string} opts.subtask — 서브태스크 설명
94
+ * @param {string} [opts.role] — 역할 (executor, designer, reviewer 등)
95
+ * @param {string} [opts.teamName] — 팀 이름
96
+ * @param {string} [opts.taskId] — Hub task ID
97
+ * @param {string} [opts.agentName] — 워커 표시 이름
98
+ * @param {string} [opts.leadName] — 리드 수신자 이름
99
+ * @param {string} [opts.mcp_profile] — MCP 프로필
100
+ * @returns {string} 슬림 래퍼 프롬프트
101
+ */
102
+ export function buildSlimWrapperPrompt(cli, opts = {}) {
103
+ const {
104
+ subtask,
105
+ role = "executor",
106
+ teamName = "tfx-team",
107
+ taskId = "",
108
+ agentName = "",
109
+ leadName = "team-lead",
110
+ mcp_profile = "auto",
111
+ } = opts;
112
+
113
+ // 셸 이스케이프
114
+ const escaped = subtask.replace(/'/g, "'\\''");
115
+
116
+ return `Bash 1회 실행 후 종료.
117
+
118
+ TFX_TEAM_NAME=${teamName} TFX_TEAM_TASK_ID=${taskId} TFX_TEAM_AGENT_NAME=${agentName} TFX_TEAM_LEAD_NAME=${leadName} bash ${ROUTE_SCRIPT} ${role} '${escaped}' ${mcp_profile}
119
+
120
+ 완료 → TaskUpdate(status: completed) + SendMessage(to: ${leadName}).
121
+ 실패 → TaskUpdate(status: failed) + SendMessage(to: ${leadName}).`;
122
+ }
123
+
86
124
  /**
87
125
  * 팀 이름 생성 (타임스탬프 기반)
88
126
  * @returns {string}
@@ -393,33 +393,49 @@ export function focusPane(target, opts = {}) {
393
393
  }
394
394
  }
395
395
 
396
- /**
397
- * 팀메이트 조작 키 바인딩 설정
398
- * - Shift+Down: 다음 팀메이트
399
- * - Shift+Up: 이전 팀메이트
400
- * - Escape: 현재 팀메이트 인터럽트(C-c)
401
- * - Ctrl+T: 태스크 목록 표시
402
- * @param {string} sessionName
396
+ /**
397
+ * 팀메이트 조작 키 바인딩 설정
398
+ * - Shift+Down: 다음 팀메이트
399
+ * - Shift+Up: 이전 팀메이트
400
+ * - Shift+Left / Shift+Tab: 이전 팀메이트 대체 키
401
+ * - Shift+Right: 다음 팀메이트 대체 키
402
+ * - Escape: 현재 팀메이트 인터럽트(C-c)
403
+ * - Ctrl+T: 태스크 목록 표시
404
+ * @param {string} sessionName
403
405
  * @param {object} opts
404
406
  * @param {boolean} opts.inProcess
405
407
  * @param {string} opts.taskListCommand
406
408
  */
407
- export function configureTeammateKeybindings(sessionName, opts = {}) {
408
- const { inProcess = false, taskListCommand = "" } = opts;
409
- const cond = `#{==:#{session_name},${sessionName}}`;
410
-
411
- if (inProcess) {
412
- // 단일 뷰(zoom) 상태에서 팀메이트 순환
413
- tmux(`bind-key -T root -n S-Down if-shell -F '${cond}' 'select-pane -t :.+ \\; resize-pane -Z' 'send-keys S-Down'`);
414
- tmux(`bind-key -T root -n S-Up if-shell -F '${cond}' 'select-pane -t :.- \\; resize-pane -Z' 'send-keys S-Up'`);
415
- } else {
416
- // 분할 뷰에서 팀메이트 순환
417
- tmux(`bind-key -T root -n S-Down if-shell -F '${cond}' 'select-pane -t :.+' 'send-keys S-Down'`);
418
- tmux(`bind-key -T root -n S-Up if-shell -F '${cond}' 'select-pane -t :.-' 'send-keys S-Up'`);
419
- }
420
-
421
- // 현재 활성 pane 인터럽트
422
- tmux(`bind-key -T root -n Escape if-shell -F '${cond}' 'send-keys C-c' 'send-keys Escape'`);
409
+ export function configureTeammateKeybindings(sessionName, opts = {}) {
410
+ const { inProcess = false, taskListCommand = "" } = opts;
411
+ const cond = `#{==:#{session_name},${sessionName}}`;
412
+
413
+ // Shift+Up이 터미널/호스트 조합에 따라 전달되지 않는 경우가 있어
414
+ // 좌/우/Shift+Tab 대체 키를 함께 바인딩한다.
415
+ const bindNext = inProcess
416
+ ? `'select-pane -t :.+ \\; resize-pane -Z'`
417
+ : `'select-pane -t :.+'`;
418
+ const bindPrev = inProcess
419
+ ? `'select-pane -t :.- \\; resize-pane -Z'`
420
+ : `'select-pane -t :.-'`;
421
+
422
+ if (inProcess) {
423
+ // 단일 뷰(zoom) 상태에서 팀메이트 순환
424
+ tmux(`bind-key -T root -n S-Down if-shell -F '${cond}' ${bindNext} 'send-keys S-Down'`);
425
+ tmux(`bind-key -T root -n S-Up if-shell -F '${cond}' ${bindPrev} 'send-keys S-Up'`);
426
+ } else {
427
+ // 분할 뷰에서 팀메이트 순환
428
+ tmux(`bind-key -T root -n S-Down if-shell -F '${cond}' ${bindNext} 'send-keys S-Down'`);
429
+ tmux(`bind-key -T root -n S-Up if-shell -F '${cond}' ${bindPrev} 'send-keys S-Up'`);
430
+ }
431
+
432
+ // 대체 키: 일부 환경에서 S-Up이 누락될 때 사용
433
+ tmux(`bind-key -T root -n S-Right if-shell -F '${cond}' ${bindNext} 'send-keys S-Right'`);
434
+ tmux(`bind-key -T root -n S-Left if-shell -F '${cond}' ${bindPrev} 'send-keys S-Left'`);
435
+ tmux(`bind-key -T root -n BTab if-shell -F '${cond}' ${bindPrev} 'send-keys BTab'`);
436
+
437
+ // 현재 활성 pane 인터럽트
438
+ tmux(`bind-key -T root -n Escape if-shell -F '${cond}' 'send-keys C-c' 'send-keys Escape'`);
423
439
 
424
440
  // 태스크 목록 토글 (tmux 3.2+ popup 우선, 실패 시 안내 메시지)
425
441
  if (taskListCommand) {
@@ -105,6 +105,9 @@ const QOS_PATH = join(homedir(), ".omc", "state", "cli_qos_profile.json");
105
105
  const ACCOUNTS_CONFIG_PATH = join(homedir(), ".omc", "router", "accounts.json");
106
106
  const ACCOUNTS_STATE_PATH = join(homedir(), ".omc", "state", "cli_accounts_state.json");
107
107
 
108
+ // tfx-team 상태 (v2.2 HUD 통합)
109
+ const TEAM_STATE_PATH = join(homedir(), ".claude", "cache", "tfx-hub", "team-state.json");
110
+
108
111
  // Claude OAuth Usage API (api.anthropic.com/api/oauth/usage)
109
112
  const CLAUDE_CREDENTIALS_PATH = join(homedir(), ".claude", ".credentials.json");
110
113
  const CLAUDE_USAGE_CACHE_PATH = join(homedir(), ".claude", "cache", "claude-usage-cache.json");
@@ -426,6 +429,54 @@ function getProviderAccountId(provider, accountsConfig, accountsState) {
426
429
  return providerConfig[0]?.id || `${provider}-main`;
427
430
  }
428
431
 
432
+ /**
433
+ * tfx-team 상태 행 생성 (v2.2 HUD 통합)
434
+ * 활성 팀이 있을 때만 행 반환, 없으면 null
435
+ * @returns {{ prefix: string, left: string, right: string } | null}
436
+ */
437
+ function getTeamRow() {
438
+ const teamState = readJson(TEAM_STATE_PATH, null);
439
+ if (!teamState || !teamState.sessionName) return null;
440
+
441
+ // 팀 생존 확인: startedAt 기준 24시간 초과면 stale로 간주
442
+ if (teamState.startedAt && (Date.now() - teamState.startedAt) > 24 * 60 * 60 * 1000) return null;
443
+
444
+ const workers = (teamState.members || []).filter((m) => m.role === "worker");
445
+ if (!workers.length) return null;
446
+
447
+ const tasks = teamState.tasks || [];
448
+ const completed = tasks.filter((t) => t.status === "completed").length;
449
+ const failed = tasks.filter((t) => t.status === "failed").length;
450
+ const total = tasks.length || workers.length;
451
+
452
+ // 경과 시간
453
+ const elapsed = teamState.startedAt
454
+ ? `${Math.round((Date.now() - teamState.startedAt) / 60000)}m`
455
+ : "";
456
+
457
+ // 멤버 상태 아이콘 요약
458
+ const memberIcons = workers.map((m) => {
459
+ const task = tasks.find((t) => t.owner === m.name);
460
+ const icon = task?.status === "completed" ? green("✓")
461
+ : task?.status === "in_progress" ? yellow("●")
462
+ : task?.status === "failed" ? red("✗")
463
+ : dim("○");
464
+ const tag = m.cli ? m.cli.charAt(0) : "?";
465
+ return `${tag}${icon}`;
466
+ }).join(" ");
467
+
468
+ // done / failed 상태 텍스트
469
+ const doneText = failed > 0
470
+ ? `${completed}/${total} ${red(`${failed}✗`)}`
471
+ : `${completed}/${total} done`;
472
+
473
+ return {
474
+ prefix: bold(claudeOrange("⬡")),
475
+ left: `team ${doneText} ${dim(elapsed)}`,
476
+ right: memberIcons,
477
+ };
478
+ }
479
+
429
480
  function renderAlignedRows(rows) {
430
481
  const rightRows = rows.filter((row) => stripAnsi(String(row.right || "")).trim().length > 0);
431
482
  const rawLeftWidth = rightRows.reduce((max, row) => Math.max(max, stripAnsi(row.left).length), 0);
@@ -791,7 +842,7 @@ async function fetchClaudeUsage(forceRefresh = false) {
791
842
  : result.status === 401 || result.status === 403 ? "auth"
792
843
  : result.error === "timeout" || result.error === "network" ? "network"
793
844
  : "unknown";
794
- writeClaudeUsageCache(existingSnapshot.data, { type: errorType, status: result.status });
845
+ writeClaudeUsageCache(existingSnapshot.data, { type: errorType, status: result.status });
795
846
  return existingSnapshot.data || null;
796
847
  }
797
848
  const usage = parseClaudeUsageResponse(result.data);
@@ -1707,6 +1758,10 @@ async function main() {
1707
1758
  geminiQuotaData, geminiEmail, geminiSv, null),
1708
1759
  ];
1709
1760
 
1761
+ // tfx-team 활성 시 팀 상태 행 추가 (v2.2)
1762
+ const teamRow = getTeamRow();
1763
+ if (teamRow) rows.push(teamRow);
1764
+
1710
1765
  // 비활성 프로바이더 dim 처리: 데이터 없으면 전체 줄 dim
1711
1766
  const codexActive = codexBuckets != null;
1712
1767
  const geminiActive = (geminiSession?.total || 0) > 0 || geminiBucket != null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "3.2.0-dev.3",
3
+ "version": "3.2.0-dev.6",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,8 @@
26
26
  ],
27
27
  "scripts": {
28
28
  "setup": "node scripts/setup.mjs",
29
- "postinstall": "node scripts/setup.mjs"
29
+ "postinstall": "node scripts/setup.mjs",
30
+ "test:route-smoke": "node --test scripts/test-tfx-route-no-claude-native.mjs"
30
31
  },
31
32
  "engines": {
32
33
  "node": ">=18.0.0"