triflux 7.0.3 → 7.0.5

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.
@@ -72,17 +72,12 @@ const MCP_PROFILE_HINTS = {
72
72
  */
73
73
  export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
74
74
  const { handoff = true, mcp } = opts;
75
- // 에이전트 역할명("executor" 등)을 CLI 타입으로 해석
76
75
  const resolvedCli = resolveCliType(cli);
77
- // MCP 프로필 힌트를 프롬프트에 삽입
78
- const mcpHint = mcp && MCP_PROFILE_HINTS[mcp] ? `\n[MCP: ${mcp}] ${MCP_PROFILE_HINTS[mcp]}` : "";
79
- // HANDOFF 지시를 프롬프트에 삽입 (opt-out 가능)
80
- const fullPrompt = handoff
81
- ? `${prompt}${mcpHint}\n\n${HANDOFF_INSTRUCTION}`
82
- : `${prompt}${mcpHint}`;
83
- // 프롬프트의 단일 인용부호를 이스케이프
76
+ const mcpHint = mcp && MCP_PROFILE_HINTS[mcp] ? ` [MCP: ${mcp}] ${MCP_PROFILE_HINTS[mcp]}` : "";
77
+ // HANDOFF 지시는 프롬프트에 삽입하지 않음 headless 후처리(processHandoff)에서 처리
78
+ // psmux send-keys 줄바꿈 문제 + codex exec "---" 인자 충돌 방지
79
+ const fullPrompt = `${prompt}${mcpHint}`;
84
80
  const escaped = fullPrompt.replace(/'/g, "''");
85
- // Clear-Host: 실행 즉시 이전 PS 프롬프트 + 명령 텍스트를 깨끗이 지움
86
81
  const cls = "Clear-Host; ";
87
82
 
88
83
  switch (resolvedCli) {
@@ -149,12 +144,13 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
149
144
  applyTrifluxTheme(sessionName);
150
145
  if (safeProgress) safeProgress({ type: "session_created", sessionName, panes: session.panes });
151
146
 
152
- // v7.0: dashboard pane — pane 0에 TUI 뷰어를 실행
147
+ // v7.0: dashboard pane — pane 0에 TUI 뷰어를 send-keys로 실행
153
148
  if (dashboard) {
154
- const viewerPath = join(import.meta.dirname || __dirname, "tui-viewer.mjs").replace(/\\/g, "/");
155
- const viewerCmd = `node "${viewerPath}" --session ${sessionName} --result-dir ${RESULT_DIR.replace(/\\/g, "/")}`;
149
+ const viewerPath = join(import.meta.dirname, "tui-viewer.mjs").replace(/\\/g, "/");
150
+ const resultDir = RESULT_DIR.replace(/\\/g, "/");
151
+ const viewerCmd = `node ${viewerPath} --session ${sessionName} --result-dir ${resultDir}`;
156
152
  try {
157
- dispatchCommand(sessionName, `${sessionName}:0.0`, viewerCmd);
153
+ psmuxExec(["send-keys", "-t", `${sessionName}:0.0`, viewerCmd, "Enter"]);
158
154
  psmuxExec(["select-pane", "-t", `${sessionName}:0.0`, "-T", "▲ dashboard"]);
159
155
  } catch { /* 무시 */ }
160
156
  }
@@ -1,87 +1,123 @@
1
1
  #!/usr/bin/env node
2
- // hub/team/tui-viewer.mjs — psmux pane용 TUI 대시보드 뷰어
3
- // 독립 프로세스로 실행되어 headless 워커 상태를 실시간 렌더링한다.
4
- //
5
- // 실행: node hub/team/tui-viewer.mjs --session <name> [--result-dir <dir>]
6
- //
7
- // 데이터 소스: result 파일 (RESULT_DIR/{session}-worker-N.txt) polling
8
- // + psmux capture-pane (워커 진행 상황)
2
+ // hub/team/tui-viewer.mjs — psmux pane용 TUI 대시보드 뷰어 v2
3
+ // 같은 psmux 세션의 워커 pane을 capture-pane으로 실시간 모니터링
9
4
 
10
5
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
6
+ import { execFileSync } from "node:child_process";
11
7
  import { join } from "node:path";
12
8
  import { tmpdir } from "node:os";
13
9
  import { createTui } from "./tui.mjs";
14
10
  import { processHandoff } from "./handoff.mjs";
15
11
 
16
- // ── 인자 파싱 ──
17
12
  const args = process.argv.slice(2);
18
13
  const sessionIdx = args.indexOf("--session");
19
- const resultDirIdx = args.indexOf("--result-dir");
20
14
  const SESSION = sessionIdx >= 0 ? args[sessionIdx + 1] : null;
21
- const RESULT_DIR = resultDirIdx >= 0 ? args[resultDirIdx + 1] : join(tmpdir(), "tfx-headless");
15
+ const RESULT_DIR = join(tmpdir(), "tfx-headless");
22
16
 
23
17
  if (!SESSION) {
24
18
  process.stderr.write("Usage: node tui-viewer.mjs --session <name>\n");
25
19
  process.exit(1);
26
20
  }
27
21
 
28
- // ── TUI 초기화 ──
29
- const tui = createTui({ refreshMs: 0 }); // 수동 렌더 (폴링 주기에 맞춤)
30
-
31
- // ── 워커 상태 폴링 ──
32
- const POLL_MS = 800;
33
- const knownWorkers = new Map(); // paneName → { resultFile, lastSize, done }
34
-
35
- function discoverWorkers() {
36
- if (!existsSync(RESULT_DIR)) return;
37
- const prefix = `${SESSION}-worker-`;
38
- for (const f of readdirSync(RESULT_DIR)) {
39
- if (!f.startsWith(prefix) || !f.endsWith(".txt")) continue;
40
- const paneName = f.replace(`${SESSION}-`, "").replace(".txt", "");
41
- if (!knownWorkers.has(paneName)) {
42
- knownWorkers.set(paneName, {
43
- resultFile: join(RESULT_DIR, f),
44
- lastSize: 0,
45
- done: false,
46
- });
47
- tui.updateWorker(paneName, { cli: "codex", status: "running" });
48
- }
49
- }
50
- }
22
+ const tui = createTui({ refreshMs: 0 });
23
+ const startTime = Date.now();
24
+ tui.setStartTime(startTime);
51
25
 
52
- function pollWorkers() {
53
- discoverWorkers();
26
+ // ── psmux pane 목록 조회 ──
27
+ function listPanes() {
28
+ try {
29
+ const out = execFileSync("psmux", [
30
+ "list-panes", "-t", SESSION, "-F",
31
+ "#{pane_index}:#{pane_title}:#{pane_pid}",
32
+ ], { encoding: "utf8", timeout: 2000 });
33
+ return out.trim().split("\n").filter(Boolean).map(line => {
34
+ const [index, title, pid] = line.split(":");
35
+ return { index: parseInt(index, 10), title: title || "", pid };
36
+ });
37
+ } catch { return []; }
38
+ }
54
39
 
55
- for (const [paneName, state] of knownWorkers) {
56
- if (state.done) continue;
40
+ // ── pane 캡처 ──
41
+ function capturePane(paneIdx, lines = 5) {
42
+ try {
43
+ return execFileSync("psmux", [
44
+ "capture-pane", "-t", `${SESSION}:0.${paneIdx}`, "-p",
45
+ ], { encoding: "utf8", timeout: 2000 }).trim().split("\n").slice(-lines).join("\n");
46
+ } catch { return ""; }
47
+ }
57
48
 
58
- let size = 0;
59
- try { size = statSync(state.resultFile).size; } catch { continue; }
49
+ // ── result 파일에서 handoff 파싱 ──
50
+ function checkResultFile(paneName) {
51
+ const resultFile = join(RESULT_DIR, `${SESSION}-${paneName}.txt`);
52
+ if (!existsSync(resultFile)) return null;
53
+ try {
54
+ const content = readFileSync(resultFile, "utf8");
55
+ if (content.trim().length === 0) return null;
56
+ return processHandoff(content, { exitCode: 0, resultFile });
57
+ } catch { return null; }
58
+ }
60
59
 
61
- if (size === state.lastSize) continue;
62
- state.lastSize = size;
60
+ // ── 메인 폴링 ──
61
+ const POLL_MS = 1000;
62
+ const workerState = new Map(); // paneName → { paneIdx, done }
63
+
64
+ function poll() {
65
+ const panes = listPanes();
66
+ // pane 0 = 대시보드 (자기 자신), pane 1+ = 워커
67
+ for (const pane of panes) {
68
+ if (pane.index === 0) continue; // 자기 자신 건너뜀
69
+ const paneName = `worker-${pane.index}`;
70
+ const existing = workerState.get(paneName);
71
+
72
+ if (existing?.done) continue;
73
+
74
+ // CLI 타입 추정 (pane title에서)
75
+ let cli = "codex";
76
+ if (pane.title.includes("gemini") || pane.title.includes("🔵")) cli = "gemini";
77
+ else if (pane.title.includes("claude") || pane.title.includes("🟠")) cli = "claude";
78
+
79
+ // result 파일 확인 (완료 여부)
80
+ const handoffResult = checkResultFile(paneName);
81
+ if (handoffResult && !handoffResult.fallback) {
82
+ workerState.set(paneName, { paneIdx: pane.index, done: true });
83
+ tui.updateWorker(paneName, {
84
+ cli,
85
+ role: pane.title,
86
+ status: handoffResult.handoff.status === "failed" ? "failed" : "completed",
87
+ handoff: handoffResult.handoff,
88
+ elapsed: Math.round((Date.now() - startTime) / 1000),
89
+ });
90
+ continue;
91
+ }
63
92
 
64
- // 결과 파일 읽기
65
- let content = "";
66
- try { content = readFileSync(state.resultFile, "utf8"); } catch { continue; }
93
+ // 진행 — pane 캡처로 스냅샷
94
+ const snapshot = capturePane(pane.index, 3);
95
+ const lastLine = snapshot.split("\n").filter(l => l.trim()).pop() || "";
67
96
 
68
- if (content.trim().length === 0) continue;
97
+ if (!existing) {
98
+ workerState.set(paneName, { paneIdx: pane.index, done: false });
99
+ }
69
100
 
70
- // handoff 파싱 시도
71
- const result = processHandoff(content, { exitCode: 0, resultFile: state.resultFile });
101
+ // result 파일이 있고 내용이 있으면 완료로 간주
102
+ const resultFile = join(RESULT_DIR, `${SESSION}-${paneName}.txt`);
103
+ let resultSize = 0;
104
+ try { resultSize = statSync(resultFile).size; } catch {}
72
105
 
73
- if (result.handoff && !result.fallback) {
74
- // handoff 블록 발견 → 완료
75
- state.done = true;
106
+ if (resultSize > 10) {
107
+ // 완료 (fallback handoff)
108
+ workerState.set(paneName, { paneIdx: pane.index, done: true });
109
+ const fb = handoffResult || { handoff: { verdict: lastLine.slice(0, 60), confidence: "low" } };
76
110
  tui.updateWorker(paneName, {
77
- status: result.handoff.status === "failed" ? "failed" : "completed",
78
- handoff: result.handoff,
111
+ cli,
112
+ role: pane.title,
113
+ status: "completed",
114
+ handoff: fb.handoff,
79
115
  elapsed: Math.round((Date.now() - startTime) / 1000),
80
116
  });
81
117
  } else {
82
- // 아직 진행 중 — 마지막 줄을 스냅샷으로
83
- const lastLine = content.trim().split("\n").filter(l => l.trim()).pop() || "";
84
118
  tui.updateWorker(paneName, {
119
+ cli,
120
+ role: pane.title,
85
121
  status: "running",
86
122
  snapshot: lastLine.slice(0, 60),
87
123
  elapsed: Math.round((Date.now() - startTime) / 1000),
@@ -92,39 +128,19 @@ function pollWorkers() {
92
128
  tui.render();
93
129
  }
94
130
 
95
- // ── 메인 루프 ──
96
- const startTime = Date.now();
97
- tui.setStartTime(startTime);
98
-
99
131
  // 초기 렌더
100
132
  tui.render();
101
133
 
102
- const timer = setInterval(pollWorkers, POLL_MS);
103
-
104
- // 모든 워커 완료 감지 10초 후 자동 종료
105
- let doneCheckTimer = setInterval(() => {
106
- if (knownWorkers.size > 0 && [...knownWorkers.values()].every(w => w.done)) {
107
- tui.render(); // 최종 렌더
108
- setTimeout(() => {
109
- tui.close();
110
- clearInterval(timer);
111
- clearInterval(doneCheckTimer);
112
- process.exit(0);
113
- }, 10000); // 10초 유지 후 종료
114
- clearInterval(doneCheckTimer);
134
+ const timer = setInterval(poll, POLL_MS);
135
+
136
+ // 모든 워커 완료 → 15유지 후 종료
137
+ const doneCheck = setInterval(() => {
138
+ if (workerState.size > 0 && [...workerState.values()].every(w => w.done)) {
139
+ tui.render();
140
+ clearInterval(doneCheck);
141
+ setTimeout(() => { tui.close(); clearInterval(timer); process.exit(0); }, 15000);
115
142
  }
116
143
  }, 2000);
117
144
 
118
- // Ctrl+C 정리
119
- process.on("SIGINT", () => {
120
- tui.close();
121
- clearInterval(timer);
122
- process.exit(0);
123
- });
124
-
125
- // 5분 안전 타임아웃
126
- setTimeout(() => {
127
- tui.close();
128
- clearInterval(timer);
129
- process.exit(0);
130
- }, 5 * 60 * 1000);
145
+ process.on("SIGINT", () => { tui.close(); clearInterval(timer); process.exit(0); });
146
+ setTimeout(() => { tui.close(); clearInterval(timer); process.exit(0); }, 10 * 60 * 1000);
package/hub/team/tui.mjs CHANGED
@@ -9,7 +9,13 @@ import {
9
9
  STATUS_ICON, CLI_ICON,
10
10
  } from "./ansi.mjs";
11
11
 
12
- const VERSION = "7.0.0";
12
+ // package.json에서 동적 로드 (실패 시 fallback)
13
+ let VERSION = "7.x";
14
+ try {
15
+ const { createRequire } = await import("node:module");
16
+ const require = createRequire(import.meta.url);
17
+ VERSION = require("../../package.json").version;
18
+ } catch { /* fallback */ }
13
19
 
14
20
  /**
15
21
  * TUI 대시보드 생성
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "7.0.3",
3
+ "version": "7.0.5",
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": {