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.
- package/hub/team/headless.mjs +9 -13
- package/hub/team/tui-viewer.mjs +100 -84
- package/hub/team/tui.mjs +7 -1
- package/package.json +1 -1
package/hub/team/headless.mjs
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
const fullPrompt =
|
|
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
|
|
155
|
-
const
|
|
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
|
-
|
|
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
|
}
|
package/hub/team/tui-viewer.mjs
CHANGED
|
@@ -1,87 +1,123 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// hub/team/tui-viewer.mjs — psmux pane용 TUI 대시보드 뷰어
|
|
3
|
-
//
|
|
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 =
|
|
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
|
-
|
|
29
|
-
const
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
93
|
+
// 진행 중 — pane 캡처로 스냅샷
|
|
94
|
+
const snapshot = capturePane(pane.index, 3);
|
|
95
|
+
const lastLine = snapshot.split("\n").filter(l => l.trim()).pop() || "";
|
|
67
96
|
|
|
68
|
-
if (
|
|
97
|
+
if (!existing) {
|
|
98
|
+
workerState.set(paneName, { paneIdx: pane.index, done: false });
|
|
99
|
+
}
|
|
69
100
|
|
|
70
|
-
//
|
|
71
|
-
const
|
|
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 (
|
|
74
|
-
//
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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(
|
|
103
|
-
|
|
104
|
-
// 모든 워커 완료
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
tui.render();
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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 대시보드 생성
|