@triflux/remote 10.1.0 → 10.25.1
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/pipe.mjs +239 -121
- package/hub/public/dashboard.html +61 -0
- package/hub/server.mjs +2165 -708
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +26 -7
- package/hub/team/ansi.mjs +245 -146
- package/hub/team/backend.mjs +84 -17
- package/hub/team/build-worker-prompt.mjs +42 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +219 -0
- package/hub/team/claude-native-bridge.mjs +784 -0
- package/hub/team/claude-session-projection.mjs +65 -0
- package/hub/team/cli/commands/attach.mjs +57 -37
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +95 -74
- package/hub/team/cli/commands/focus.mjs +80 -53
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +31 -24
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -33
- package/hub/team/cli/commands/start/parse-args.mjs +89 -12
- package/hub/team/cli/commands/start/start-headless.mjs +125 -37
- package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
- package/hub/team/cli/commands/start/start-mux.mjs +122 -73
- package/hub/team/cli/commands/start/start-wt.mjs +86 -69
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +52 -30
- package/hub/team/cli/services/attach-fallback.mjs +74 -54
- package/hub/team/cli/services/hub-client.mjs +44 -16
- package/hub/team/cli/services/member-selector.mjs +37 -30
- package/hub/team/cli/services/native-control.mjs +144 -116
- package/hub/team/cli/services/runtime-mode.mjs +55 -1
- package/hub/team/cli/services/state-store.mjs +66 -9
- package/hub/team/cli/services/task-model.mjs +40 -30
- package/hub/team/codex-review.mjs +521 -0
- package/hub/team/conductor-mesh-bridge.mjs +25 -24
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +1379 -671
- package/hub/team/consensus-meta.mjs +176 -0
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +20 -118
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +241 -0
- package/hub/team/extract-completion-payload.mjs +110 -0
- package/hub/team/git-preflight.mjs +286 -0
- package/hub/team/handoff.mjs +49 -18
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +2065 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/launcher-template.mjs +23 -13
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/native-supervisor.mjs +90 -26
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +147 -78
- package/hub/team/orchestrator.mjs +173 -161
- package/hub/team/pane.mjs +43 -15
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1020 -279
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +200 -0
- package/hub/team/schemas/worker-completion.json +39 -0
- package/hub/team/sentinel-capture.mjs +127 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/session.mjs +605 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-cli.mjs +390 -0
- package/hub/team/swarm-hypervisor.mjs +1522 -193
- package/hub/team/swarm-intent.mjs +216 -0
- package/hub/team/swarm-locks.mjs +140 -25
- package/hub/team/swarm-planner.mjs +121 -21
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/swarm-reconciler.mjs +128 -23
- package/hub/team/synapse-cli.mjs +458 -0
- package/hub/team/synapse-http.mjs +60 -0
- package/hub/team/synapse-registry.mjs +329 -0
- package/hub/team/terminal-opener.mjs +216 -0
- package/hub/team/tui-core.mjs +288 -0
- package/hub/team/tui-lite.mjs +234 -146
- package/hub/team/tui-remote-adapter.mjs +18 -34
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +104 -44
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +927 -437
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worker-signal.mjs +263 -0
- package/hub/team/worker-signal.types.d.ts +52 -0
- package/hub/team/worktree-lifecycle.mjs +517 -63
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +474 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-app-server-worker.mjs +1015 -0
- package/hub/workers/codex-mcp.mjs +724 -507
- package/hub/workers/delegator-mcp.mjs +662 -335
- package/hub/workers/factory.mjs +100 -21
- package/hub/workers/gemini-worker.mjs +179 -86
- package/hub/workers/interface.mjs +23 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +22 -16
- package/package.json +4 -3
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +223 -0
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +59 -0
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +269 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +119 -0
- package/scripts/lib/handoff.mjs +187 -0
- package/scripts/lib/hook-utils.mjs +16 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +195 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +883 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-guard-engine.mjs +2120 -0
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +89 -0
- package/scripts/lib/mcp-server-catalog.mjs +355 -0
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +179 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +226 -0
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +375 -0
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -1,161 +1,173 @@
|
|
|
1
|
-
// hub/team/orchestrator.mjs — 작업 분배 + 프롬프트 구성
|
|
2
|
-
// 의존성: pane.mjs만 사용
|
|
3
|
-
import { injectPrompt } from "./pane.mjs";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 작업 분해 (LLM 없이 구분자 기반)
|
|
7
|
-
* @param {string} taskDescription — 전체 작업 설명
|
|
8
|
-
* @param {number} agentCount — 에이전트 수
|
|
9
|
-
* @returns {string[]} 각 에이전트의 서브태스크
|
|
10
|
-
*/
|
|
11
|
-
export function decomposeTask(taskDescription, agentCount) {
|
|
12
|
-
if (agentCount <= 0) return [];
|
|
13
|
-
if (agentCount === 1) return [taskDescription];
|
|
14
|
-
|
|
15
|
-
// '+', ',', '\n' 기준으로 분리
|
|
16
|
-
const parts = taskDescription
|
|
17
|
-
.split(/[+,\n]+/)
|
|
18
|
-
.map((s) => s.trim())
|
|
19
|
-
.filter(Boolean);
|
|
20
|
-
|
|
21
|
-
if (parts.length === 0) return [taskDescription];
|
|
22
|
-
|
|
23
|
-
// 에이전트보다 서브태스크가 적으면 마지막 에이전트에 전체 태스크 부여
|
|
24
|
-
if (parts.length < agentCount) {
|
|
25
|
-
const result = [...parts];
|
|
26
|
-
while (result.length < agentCount) {
|
|
27
|
-
result.push(taskDescription);
|
|
28
|
-
}
|
|
29
|
-
return result;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// 에이전트보다 서브태스크가 많으면 앞에서부터 N개, 나머지는 마지막에 합침
|
|
33
|
-
if (parts.length > agentCount) {
|
|
34
|
-
const result = parts.slice(0, agentCount - 1);
|
|
35
|
-
result.push(parts.slice(agentCount - 1).join(" + "));
|
|
36
|
-
return result;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return parts;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 리드(보통 claude) 초기 프롬프트 생성
|
|
44
|
-
* @param {string} taskDescription
|
|
45
|
-
* @param {object} config
|
|
46
|
-
* @param {string} config.agentId
|
|
47
|
-
* @param {string} config.hubUrl
|
|
48
|
-
* @param {string} config.teammateMode
|
|
49
|
-
* @param {Array<{agentId:string, cli:string, subtask:string}>} config.workers
|
|
50
|
-
* @returns {string}
|
|
51
|
-
*/
|
|
52
|
-
export function buildLeadPrompt(taskDescription, config) {
|
|
53
|
-
const { agentId, teammateMode = "tmux", workers = [] } = config;
|
|
54
|
-
|
|
55
|
-
const roster =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
*
|
|
86
|
-
* @param {
|
|
87
|
-
* @param {
|
|
88
|
-
* @param {string} config.
|
|
89
|
-
* @param {string} config.
|
|
90
|
-
* @
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
*
|
|
118
|
-
* @param {
|
|
119
|
-
* @param {
|
|
120
|
-
* @param {
|
|
121
|
-
* @param {
|
|
122
|
-
* @param {string} opts.
|
|
123
|
-
* @
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
1
|
+
// hub/team/orchestrator.mjs — 작업 분배 + 프롬프트 구성
|
|
2
|
+
// 의존성: pane.mjs만 사용
|
|
3
|
+
import { injectPrompt as defaultInjectPrompt } from "./pane.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 작업 분해 (LLM 없이 구분자 기반)
|
|
7
|
+
* @param {string} taskDescription — 전체 작업 설명
|
|
8
|
+
* @param {number} agentCount — 에이전트 수
|
|
9
|
+
* @returns {string[]} 각 에이전트의 서브태스크
|
|
10
|
+
*/
|
|
11
|
+
export function decomposeTask(taskDescription, agentCount) {
|
|
12
|
+
if (agentCount <= 0) return [];
|
|
13
|
+
if (agentCount === 1) return [taskDescription];
|
|
14
|
+
|
|
15
|
+
// '+', ',', '\n' 기준으로 분리
|
|
16
|
+
const parts = taskDescription
|
|
17
|
+
.split(/[+,\n]+/)
|
|
18
|
+
.map((s) => s.trim())
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
|
|
21
|
+
if (parts.length === 0) return [taskDescription];
|
|
22
|
+
|
|
23
|
+
// 에이전트보다 서브태스크가 적으면 마지막 에이전트에 전체 태스크 부여
|
|
24
|
+
if (parts.length < agentCount) {
|
|
25
|
+
const result = [...parts];
|
|
26
|
+
while (result.length < agentCount) {
|
|
27
|
+
result.push(taskDescription);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 에이전트보다 서브태스크가 많으면 앞에서부터 N개, 나머지는 마지막에 합침
|
|
33
|
+
if (parts.length > agentCount) {
|
|
34
|
+
const result = parts.slice(0, agentCount - 1);
|
|
35
|
+
result.push(parts.slice(agentCount - 1).join(" + "));
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return parts;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 리드(보통 claude) 초기 프롬프트 생성
|
|
44
|
+
* @param {string} taskDescription
|
|
45
|
+
* @param {object} config
|
|
46
|
+
* @param {string} config.agentId
|
|
47
|
+
* @param {string} config.hubUrl
|
|
48
|
+
* @param {string} config.teammateMode
|
|
49
|
+
* @param {Array<{agentId:string, cli:string, subtask:string}>} config.workers
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function buildLeadPrompt(taskDescription, config) {
|
|
53
|
+
const { agentId, teammateMode = "tmux", workers = [] } = config;
|
|
54
|
+
|
|
55
|
+
const roster =
|
|
56
|
+
workers
|
|
57
|
+
.map((w, i) => `${i + 1}. ${w.agentId} (${w.cli}) — ${w.subtask}`)
|
|
58
|
+
.join("\n") || "- (워커 없음)";
|
|
59
|
+
|
|
60
|
+
const workerIds = workers.map((w) => w.agentId).join(", ");
|
|
61
|
+
|
|
62
|
+
const bridgePath = "node hub/bridge.mjs";
|
|
63
|
+
|
|
64
|
+
return `리드 에이전트: ${agentId}
|
|
65
|
+
|
|
66
|
+
목표: ${taskDescription}
|
|
67
|
+
모드: ${teammateMode}
|
|
68
|
+
|
|
69
|
+
워커:
|
|
70
|
+
${roster}
|
|
71
|
+
|
|
72
|
+
규칙:
|
|
73
|
+
- 가능한 짧고 핵심만 지시/요약(토큰 절약)
|
|
74
|
+
- 워커 제어:
|
|
75
|
+
${bridgePath} result --agent ${agentId} --topic lead.control
|
|
76
|
+
- 워커 결과 수집:
|
|
77
|
+
${bridgePath} context --agent ${agentId} --max 20
|
|
78
|
+
- 최종 결과는 topic="task.result"를 모아 통합
|
|
79
|
+
|
|
80
|
+
워커 ID: ${workerIds || "(없음)"}
|
|
81
|
+
지금 즉시 워커를 배정하고 병렬 진행을 관리하라.`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 워커 초기 프롬프트 생성
|
|
86
|
+
* @param {string} subtask — 이 에이전트의 서브태스크
|
|
87
|
+
* @param {object} config
|
|
88
|
+
* @param {string} config.cli — codex/gemini/claude
|
|
89
|
+
* @param {string} config.agentId — 에이전트 식별자
|
|
90
|
+
* @param {string} config.hubUrl — Hub URL
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
export function buildPrompt(subtask, config) {
|
|
94
|
+
const { cli, agentId, hubUrl } = config;
|
|
95
|
+
|
|
96
|
+
const _hubBase = hubUrl.replace("/mcp", "");
|
|
97
|
+
|
|
98
|
+
const bridgePath = "node hub/bridge.mjs";
|
|
99
|
+
|
|
100
|
+
return `워커: ${agentId} (${cli})
|
|
101
|
+
작업: ${subtask}
|
|
102
|
+
|
|
103
|
+
필수 규칙:
|
|
104
|
+
1) 간결하게 작업(불필요한 장문 설명 금지)
|
|
105
|
+
2) 시작 즉시 등록:
|
|
106
|
+
${bridgePath} register --agent ${agentId} --cli ${cli} --topics lead.control,task.result
|
|
107
|
+
3) 주기적으로 수신함 확인:
|
|
108
|
+
${bridgePath} context --agent ${agentId} --max 10
|
|
109
|
+
4) lead.control 수신 시 즉시 반응 (interrupt/stop/pause/resume)
|
|
110
|
+
5) 완료 시 결과 발행:
|
|
111
|
+
${bridgePath} result --agent ${agentId} --topic task.result --file <출력파일>
|
|
112
|
+
|
|
113
|
+
지금 작업을 시작하라.`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 팀 오케스트레이션 실행 — 각 pane에 프롬프트 주입
|
|
118
|
+
* @param {string} sessionName — tmux 세션 이름
|
|
119
|
+
* @param {Array<{target: string, cli: string, subtask: string}>} assignments
|
|
120
|
+
* @param {object} opts
|
|
121
|
+
* @param {string} opts.hubUrl — Hub URL
|
|
122
|
+
* @param {{target:string, cli:string, task:string}|null} opts.lead
|
|
123
|
+
* @param {string} opts.teammateMode
|
|
124
|
+
* @returns {Promise<void>}
|
|
125
|
+
*/
|
|
126
|
+
export async function orchestrate(sessionName, assignments, opts = {}) {
|
|
127
|
+
const {
|
|
128
|
+
hubUrl = "http://127.0.0.1:27888/mcp",
|
|
129
|
+
lead = null,
|
|
130
|
+
teammateMode = "tmux",
|
|
131
|
+
injectPrompt = defaultInjectPrompt,
|
|
132
|
+
} = opts;
|
|
133
|
+
|
|
134
|
+
const workers = assignments.map(({ target, cli, subtask }) => ({
|
|
135
|
+
target,
|
|
136
|
+
cli,
|
|
137
|
+
subtask,
|
|
138
|
+
agentId: `${cli}-${target.split(".").pop()}`,
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
if (lead?.target) {
|
|
142
|
+
const leadAgentId = `${lead.cli || "claude"}-${lead.target.split(".").pop()}`;
|
|
143
|
+
const leadPrompt = buildLeadPrompt(lead.task || "팀 작업 조율", {
|
|
144
|
+
agentId: leadAgentId,
|
|
145
|
+
hubUrl,
|
|
146
|
+
teammateMode,
|
|
147
|
+
workers: workers.map((w) => ({
|
|
148
|
+
agentId: w.agentId,
|
|
149
|
+
cli: w.cli,
|
|
150
|
+
subtask: w.subtask,
|
|
151
|
+
})),
|
|
152
|
+
});
|
|
153
|
+
injectPrompt(lead.target, leadPrompt, {
|
|
154
|
+
useFileRef: true,
|
|
155
|
+
cli: lead.cli,
|
|
156
|
+
});
|
|
157
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const worker of workers) {
|
|
161
|
+
const prompt = buildPrompt(worker.subtask, {
|
|
162
|
+
cli: worker.cli,
|
|
163
|
+
agentId: worker.agentId,
|
|
164
|
+
hubUrl,
|
|
165
|
+
sessionName,
|
|
166
|
+
});
|
|
167
|
+
injectPrompt(worker.target, prompt, {
|
|
168
|
+
useFileRef: true,
|
|
169
|
+
cli: worker.cli,
|
|
170
|
+
});
|
|
171
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
172
|
+
}
|
|
173
|
+
}
|
package/hub/team/pane.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// hub/team/pane.mjs — pane별 CLI 실행 + stdin 주입
|
|
2
2
|
// 의존성: child_process, fs, os, path (Node.js 내장)만 사용
|
|
3
|
-
import {
|
|
4
|
-
import { join } from "node:path";
|
|
3
|
+
import { mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
6
|
-
import {
|
|
7
|
-
import { psmuxExec } from "./psmux.mjs";
|
|
8
|
-
|
|
5
|
+
import { join } from "node:path";
|
|
9
6
|
import { buildExecArgs } from "@triflux/core/hub/codex-adapter.mjs";
|
|
7
|
+
import { psmuxExec } from "./psmux.mjs";
|
|
8
|
+
import { detectMultiplexer, tmuxExec } from "./session.mjs";
|
|
10
9
|
|
|
11
10
|
function quoteArg(value) {
|
|
12
11
|
return `"${String(value).replace(/"/g, '\\"')}"`;
|
|
@@ -91,34 +90,61 @@ export function startCliInPane(target, command) {
|
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
93
|
+
* psmux `@file` 참조 주입이 가능한 CLI인지 판정한다.
|
|
94
|
+
*
|
|
95
|
+
* Codex TUI는 `@` 입력이 있으면 sync_file_search_popup 경로로 FileSearchPopup을
|
|
96
|
+
* 활성화한다 (call-site `codex-rs/tui/src/bottom_pane/chat_composer.rs:3082`;
|
|
97
|
+
* 본체 L3271-3313 에서 `ActivePopup::File(popup)` 분기; 빈 query 상태는
|
|
98
|
+
* `file_search_popup.rs:54-63` 의 `set_empty_prompt()` 가 정의). 팝업이 떠 있는
|
|
99
|
+
* 동안의 Enter 처리는 같은 파일 composer의 Enter handler (`chat_composer.rs:1585-1645`
|
|
100
|
+
* 기준) 에서 popup 선택 path insert 또는 dismiss 로 분기하므로 prompt submit 으로
|
|
101
|
+
* 전달되지 않는다. Gemini CLI의 `@path`는 공식 client-side file-content inject
|
|
102
|
+
* 이므로 유지. Codex TUI에서 `@path` 선택은 path 문자열만 textarea에 insert할
|
|
103
|
+
* 뿐 Gemini처럼 파일 내용을 inject하지 않으며, 별도 file-injection slash
|
|
104
|
+
* command 도 존재하지 않음 (slash_commands / prompt_args / skill_popup /
|
|
105
|
+
* command_popup 소스 전수 grep 결과 0건, 2026-04-19 검증 — absence-based).
|
|
106
|
+
*
|
|
107
|
+
* @param {{ multiplexer: string, useFileRef: boolean, cli: string|null }} args
|
|
98
108
|
*/
|
|
109
|
+
export function shouldUseFileRef({ multiplexer, useFileRef, cli }) {
|
|
110
|
+
return multiplexer === "psmux" && useFileRef && cli !== "codex";
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
/**
|
|
100
114
|
* pane에 프롬프트 주입
|
|
101
115
|
* @param {string} target — 예: tfx-multi-abc:0.1
|
|
102
116
|
* @param {string} prompt — 주입할 텍스트
|
|
103
117
|
* @param {object} [opts]
|
|
104
|
-
* @param {boolean} [opts.useFileRef] — true면 TUI용 @file 참조 방식 (psmux 전용)
|
|
118
|
+
* @param {boolean} [opts.useFileRef] — true면 TUI용 @file 참조 방식 요청 (psmux 전용). Codex에서는 자동으로 paste-buffer 경로로 fallback.
|
|
119
|
+
* @param {'codex'|'gemini'|'claude'|null} [opts.cli] — 대상 CLI. Codex일 때 @ intercept를 회피하기 위해 paste-buffer 경로를 강제한다.
|
|
105
120
|
*/
|
|
106
|
-
export function injectPrompt(
|
|
121
|
+
export function injectPrompt(
|
|
122
|
+
target,
|
|
123
|
+
prompt,
|
|
124
|
+
{ useFileRef = false, cli = null } = {},
|
|
125
|
+
) {
|
|
107
126
|
const tmpDir = join(tmpdir(), "tfx-multi");
|
|
108
127
|
mkdirSync(tmpDir, { recursive: true });
|
|
109
128
|
|
|
110
129
|
const safeTarget = target.replace(/[:.]/g, "-");
|
|
111
130
|
const tmpFile = join(tmpDir, `prompt-${safeTarget}-${Date.now()}.txt`);
|
|
112
131
|
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
const multiplexer = detectMultiplexer();
|
|
133
|
+
|
|
134
|
+
// psmux + TUI + CLI별 @file 지원: @path를 literal paste하고 Enter로 확정.
|
|
135
|
+
// Codex는 @가 file search popup을 intercept하므로 paste-buffer 경로로 fallback.
|
|
136
|
+
if (shouldUseFileRef({ multiplexer, useFileRef, cli })) {
|
|
115
137
|
writeFileSync(tmpFile, prompt, "utf8");
|
|
116
138
|
const filePath = tmpFile.replace(/\\/g, "/");
|
|
117
139
|
psmuxExec(["select-pane", "-t", target]);
|
|
118
140
|
psmuxExec(["send-keys", "-t", target, "-l", `@${filePath}`]);
|
|
119
141
|
psmuxExec(["send-keys", "-t", target, "Enter"]);
|
|
120
142
|
// TUI가 파일을 읽을 시간을 주고 정리
|
|
121
|
-
setTimeout(() => {
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
try {
|
|
145
|
+
unlinkSync(tmpFile);
|
|
146
|
+
} catch {}
|
|
147
|
+
}, 10000);
|
|
122
148
|
return;
|
|
123
149
|
}
|
|
124
150
|
|
|
@@ -139,7 +165,9 @@ export function injectPrompt(target, prompt, { useFileRef = false } = {}) {
|
|
|
139
165
|
muxExec(`paste-buffer -t ${target}`);
|
|
140
166
|
muxExec(`send-keys -t ${target} Enter`);
|
|
141
167
|
} finally {
|
|
142
|
-
try {
|
|
168
|
+
try {
|
|
169
|
+
unlinkSync(tmpFile);
|
|
170
|
+
} catch {}
|
|
143
171
|
}
|
|
144
172
|
}
|
|
145
173
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// hub/team/process-cleanup.mjs — 고아 node/python 프로세스 감지 및 정리
|
|
2
2
|
// Windows: Get-CimInstance Win32_Process로 parent PID + cmdLine 접근
|
|
3
3
|
// Unix: ps aux 파싱
|
|
4
|
-
import { execFile as nodeExecFile } from "node:child_process";
|
|
4
|
+
import { execFileSync, execFile as nodeExecFile } from "node:child_process";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { IS_WINDOWS } from "@triflux/core/hub/platform.mjs";
|
|
7
7
|
|
|
@@ -12,11 +12,37 @@ const execFileAsync = promisify(nodeExecFile);
|
|
|
12
12
|
const TARGET_PROCESS_NAMES = ["node", "python", "python3"];
|
|
13
13
|
const SIGTERM_GRACE_MS = 5000;
|
|
14
14
|
|
|
15
|
+
export function forceKillPid(
|
|
16
|
+
pid,
|
|
17
|
+
{
|
|
18
|
+
isWindows = IS_WINDOWS,
|
|
19
|
+
execFileSyncFn = execFileSync,
|
|
20
|
+
killFn = process.kill,
|
|
21
|
+
} = {},
|
|
22
|
+
) {
|
|
23
|
+
if (isWindows) {
|
|
24
|
+
try {
|
|
25
|
+
execFileSyncFn("taskkill", ["/F", "/T", "/PID", String(pid)], {
|
|
26
|
+
stdio: "ignore",
|
|
27
|
+
timeout: 5000,
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
} catch (taskkillError) {
|
|
32
|
+
try {
|
|
33
|
+
killFn(pid);
|
|
34
|
+
return;
|
|
35
|
+
} catch {
|
|
36
|
+
throw taskkillError;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
killFn(pid, "SIGKILL");
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
// cmdLine 패턴 기반 화이트리스트 (고아 후보에서 제외)
|
|
16
|
-
const WHITELIST_CMDLINE = [
|
|
17
|
-
/oh-my-claudecode/i,
|
|
18
|
-
/triflux[\\/]hub[\\/]s/i,
|
|
19
|
-
];
|
|
45
|
+
const WHITELIST_CMDLINE = [/oh-my-claudecode/i, /triflux[\\/]hub[\\/]s/i];
|
|
20
46
|
|
|
21
47
|
// 프로세스명 기반 화이트리스트
|
|
22
48
|
const WHITELIST_NAMES = ["claude", "CCXProcess"];
|
|
@@ -44,15 +70,13 @@ async function queryWindowsProcesses(execFileFn) {
|
|
|
44
70
|
const raw = JSON.parse(stdout.trim());
|
|
45
71
|
const items = Array.isArray(raw) ? raw : [raw];
|
|
46
72
|
|
|
47
|
-
return items
|
|
48
|
-
.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
ramMB: Math.round((Number(p.WorkingSetSize) || 0) / 1024 / 1024),
|
|
55
|
-
}));
|
|
73
|
+
return items.filter(Boolean).map((p) => ({
|
|
74
|
+
pid: Number(p.ProcessId),
|
|
75
|
+
name: String(p.Name || "").replace(/\.exe$/i, ""),
|
|
76
|
+
parentPid: Number(p.ParentProcessId) || 0,
|
|
77
|
+
cmdLine: String(p.CommandLine || ""),
|
|
78
|
+
ramMB: Math.round((Number(p.WorkingSetSize) || 0) / 1024 / 1024),
|
|
79
|
+
}));
|
|
56
80
|
}
|
|
57
81
|
|
|
58
82
|
/**
|
|
@@ -63,7 +87,7 @@ async function queryWindowsProcesses(execFileFn) {
|
|
|
63
87
|
async function queryUnixProcesses(execFileFn) {
|
|
64
88
|
const { stdout } = await execFileFn(
|
|
65
89
|
"ps",
|
|
66
|
-
["-eo", "pid
|
|
90
|
+
["-eo", "pid=,ppid=,rss=,comm=,args="],
|
|
67
91
|
{ encoding: "utf8", timeout: 10_000 },
|
|
68
92
|
);
|
|
69
93
|
|
|
@@ -225,7 +249,11 @@ export async function findOrphanProcesses(opts = {}) {
|
|
|
225
249
|
const nameLower = p.name.toLowerCase();
|
|
226
250
|
|
|
227
251
|
// 대상 프로세스만
|
|
228
|
-
if (
|
|
252
|
+
if (
|
|
253
|
+
!TARGET_PROCESS_NAMES.some(
|
|
254
|
+
(t) => nameLower === t || nameLower === `${t}.exe`,
|
|
255
|
+
)
|
|
256
|
+
) {
|
|
229
257
|
return false;
|
|
230
258
|
}
|
|
231
259
|
|
|
@@ -275,6 +303,12 @@ export async function findOrphanProcesses(opts = {}) {
|
|
|
275
303
|
export function createProcessCleanup(opts = {}) {
|
|
276
304
|
const execFileFn = opts.execFileFn ?? execFileAsync;
|
|
277
305
|
const dryRun = opts.dryRun ?? false;
|
|
306
|
+
const isWindows = opts.isWindows ?? IS_WINDOWS;
|
|
307
|
+
const execFileSyncFn = opts.execFileSyncFn ?? execFileSync;
|
|
308
|
+
const killFn = opts.killFn ?? process.kill;
|
|
309
|
+
const sleepFn =
|
|
310
|
+
opts.sleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
311
|
+
const sigtermGraceMs = opts.sigtermGraceMs ?? SIGTERM_GRACE_MS;
|
|
278
312
|
|
|
279
313
|
let lastOrphans = [];
|
|
280
314
|
|
|
@@ -294,35 +328,45 @@ export function createProcessCleanup(opts = {}) {
|
|
|
294
328
|
/**
|
|
295
329
|
* 마지막 scan 결과의 프로세스를 kill한다.
|
|
296
330
|
* dryRun=true이면 kill 없이 목록만 반환한다.
|
|
297
|
-
* SIGTERM → 5s 대기 → SIGKILL 순서.
|
|
331
|
+
* SIGTERM → 5s 대기 → 강제 종료(taskkill/SIGKILL) 순서.
|
|
298
332
|
* @returns {Promise<Array<{pid,name,killed,error}>>}
|
|
299
333
|
*/
|
|
300
334
|
async function kill() {
|
|
301
335
|
if (dryRun) {
|
|
302
|
-
return lastOrphans.map((p) => ({
|
|
336
|
+
return lastOrphans.map((p) => ({
|
|
337
|
+
pid: p.pid,
|
|
338
|
+
name: p.name,
|
|
339
|
+
killed: false,
|
|
340
|
+
dryRun: true,
|
|
341
|
+
}));
|
|
303
342
|
}
|
|
304
343
|
|
|
305
344
|
const results = await Promise.all(
|
|
306
345
|
lastOrphans.map(async (p) => {
|
|
307
346
|
try {
|
|
308
347
|
// SIGTERM
|
|
309
|
-
|
|
348
|
+
killFn(p.pid, "SIGTERM");
|
|
310
349
|
|
|
311
|
-
// 5초 대기 후 살아있으면
|
|
312
|
-
await
|
|
350
|
+
// 5초 대기 후 살아있으면 강제 종료
|
|
351
|
+
await sleepFn(sigtermGraceMs);
|
|
313
352
|
|
|
314
353
|
try {
|
|
315
354
|
// 프로세스가 아직 살아있는지 확인 (signal 0)
|
|
316
|
-
|
|
317
|
-
// 여전히 살아있음 → SIGKILL
|
|
318
|
-
|
|
355
|
+
killFn(p.pid, 0);
|
|
356
|
+
// 여전히 살아있음 → Windows는 taskkill/process.kill, 그 외는 SIGKILL
|
|
357
|
+
forceKillPid(p.pid, { isWindows, execFileSyncFn, killFn });
|
|
319
358
|
} catch {
|
|
320
359
|
// ESRCH: 이미 종료됨 — 정상
|
|
321
360
|
}
|
|
322
361
|
|
|
323
362
|
return { pid: p.pid, name: p.name, killed: true };
|
|
324
363
|
} catch (err) {
|
|
325
|
-
return {
|
|
364
|
+
return {
|
|
365
|
+
pid: p.pid,
|
|
366
|
+
name: p.name,
|
|
367
|
+
killed: false,
|
|
368
|
+
error: String(err.message || err),
|
|
369
|
+
};
|
|
326
370
|
}
|
|
327
371
|
}),
|
|
328
372
|
);
|