@triflux/remote 10.0.0 → 10.1.0

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.
@@ -290,4 +290,4 @@ export function createNotifier(opts = {}) {
290
290
  });
291
291
 
292
292
  return createNotifierInstance(normalizeChannels(opts.channels, env), deps);
293
- }
293
+ }
@@ -50,7 +50,7 @@ export function decomposeTask(taskDescription, agentCount) {
50
50
  * @returns {string}
51
51
  */
52
52
  export function buildLeadPrompt(taskDescription, config) {
53
- const { agentId, hubUrl, teammateMode = "tmux", workers = [] } = config;
53
+ const { agentId, teammateMode = "tmux", workers = [] } = config;
54
54
 
55
55
  const roster = workers
56
56
  .map((w, i) => `${i + 1}. ${w.agentId} (${w.cli}) — ${w.subtask}`)
@@ -92,7 +92,7 @@ ${roster}
92
92
  export function buildPrompt(subtask, config) {
93
93
  const { cli, agentId, hubUrl } = config;
94
94
 
95
- const hubBase = hubUrl.replace("/mcp", "");
95
+ const _hubBase = hubUrl.replace("/mcp", "");
96
96
 
97
97
  const bridgePath = "node hub/bridge.mjs";
98
98
 
@@ -196,7 +196,12 @@ function ensureCaptureHelper() {
196
196
  "}",
197
197
  "",
198
198
  "# Force UTF-8 encoding — CP949 등 non-UTF-8 codepage에서 Gemini stdout 캡처 실패 방지",
199
+ "# InputEncoding: pipe-pane에서 읽어들이는 바이트 해석",
200
+ "# OutputEncoding: 네이티브 명령(gemini/codex CLI)의 stdout 디코딩",
201
+ "# $OutputEncoding: PowerShell 파이프라인 기본 인코딩 (BOM 없는 UTF-8)",
199
202
  "[Console]::InputEncoding = [System.Text.Encoding]::UTF8",
203
+ "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
204
+ "$OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
200
205
  "",
201
206
  "$reader = [Console]::In",
202
207
  "while (($line = $reader.ReadLine()) -ne $null) {",
@@ -514,6 +519,23 @@ export function createPsmuxSession(sessionName, opts = {}) {
514
519
  psmuxExec(["select-pane", "-t", pane, "-T", toPaneTitle(index)]);
515
520
  });
516
521
 
522
+ // CP949 등 non-UTF-8 codepage 환경에서 CLI stdout 깨짐 방지:
523
+ // pane 생성 직후 UTF-8 인코딩을 강제 설정한다.
524
+ // chcp 65001: 프로세스 코드 페이지를 UTF-8로 전환
525
+ // OutputEncoding: 네이티브 명령(gemini/codex) stdout 디코딩
526
+ // $OutputEncoding: PowerShell 파이프라인 기본 인코딩
527
+ if (IS_WINDOWS) {
528
+ const encodingInit = [
529
+ "chcp 65001 > $null",
530
+ "[Console]::InputEncoding = [System.Text.Encoding]::UTF8",
531
+ "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
532
+ "$OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
533
+ ].join("; ");
534
+ panes.forEach((paneId) => {
535
+ sendLiteralToPane(paneId, encodingInit, true);
536
+ });
537
+ }
538
+
517
539
  return { sessionName, panes };
518
540
  }
519
541
 
@@ -857,15 +879,55 @@ export function startCapture(sessionName, paneNameOrTarget) {
857
879
  * @param {string} cmd
858
880
  * @returns {string}
859
881
  */
882
+ // Windows: PowerShell의 bash가 WSL을 가리킬 수 있음. Git Bash를 명시적으로 사용.
883
+ // WSL bash에서는 /c/Users/... 경로가 유효하지 않아 exit 127 발생.
884
+ // psmux pane 용 (PowerShell → bash 실행): & "path" 형식
885
+ // Node.js execSync 용 (직접 실행): "path" 형식
886
+ const _gitBashPath = (() => {
887
+ if (!IS_WINDOWS) return null;
888
+ const candidates = [
889
+ "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
890
+ "C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe",
891
+ ];
892
+ for (const p of candidates) {
893
+ if (existsSync(p)) return p;
894
+ }
895
+ return null;
896
+ })();
897
+ // PowerShell call operator(&)가 필요: & "C:\...\bash.exe" -c '...'
898
+ const GIT_BASH_BIN_PS = _gitBashPath ? `& "${_gitBashPath}"` : "bash";
899
+ // Node.js execSync에서 직접 실행할 때는 따옴표만
900
+ const GIT_BASH_BIN_EXEC = _gitBashPath ? `"${_gitBashPath}"` : "bash";
901
+
902
+ // CLI 바이너리 절대 경로 캐시 (세션 중 1회만 resolve)
903
+ const _cliPathCache = new Map();
904
+ function resolveCliAbsPath(name) {
905
+ if (_cliPathCache.has(name)) return _cliPathCache.get(name);
906
+ try {
907
+ const resolved = childProcess
908
+ .execSync(`${GIT_BASH_BIN_EXEC} -c "which ${name}"`, { encoding: "utf8", timeout: 3000 })
909
+ .trim();
910
+ if (resolved) _cliPathCache.set(name, resolved);
911
+ return resolved || name;
912
+ } catch {
913
+ return name;
914
+ }
915
+ }
916
+
860
917
  function wrapCliForBash(cmd) {
861
918
  const trimmed = cmd.trimStart();
862
919
  // PowerShell 구문(Clear-Host, Get-Content 등) 또는 completion token이 포함되면 PowerShell 직통
863
920
  if (/Clear-Host|Get-Content|__TRIFLUX_DONE__/i.test(trimmed)) return cmd;
864
- const isCli = /\b(codex|gemini)\b/u.test(trimmed);
865
- if (!isCli) return cmd;
921
+ const cliMatch = trimmed.match(/^(codex|gemini)\b/u);
922
+ if (!cliMatch) return cmd;
923
+ // Node.js 측에서 절대 경로 resolve → psmux pane의 bash PATH 불일치 문제 해결 (exit 127)
924
+ const absPath = resolveCliAbsPath(cliMatch[1]);
925
+ const resolved = absPath !== cliMatch[1]
926
+ ? trimmed.replace(new RegExp(`^${cliMatch[1]}\\b`), absPath)
927
+ : trimmed;
866
928
  // 단일 따옴표 이스케이프: ' → '\''
867
- const escaped = trimmed.replace(/'/g, "'\\''");
868
- return `bash -c '${escaped}'`;
929
+ const escaped = resolved.replace(/'/g, "'\\''");
930
+ return `${GIT_BASH_BIN_PS} -c '${escaped}'`;
869
931
  }
870
932
 
871
933
  export function dispatchCommand(sessionName, paneNameOrTarget, commandText) {
@@ -1230,7 +1292,7 @@ export function captureWorkerOutput(sessionName, workerName, lines = 50) {
1230
1292
 
1231
1293
  // ─── CLI 진입점 ───
1232
1294
 
1233
- if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
1295
+ if (process.argv[1]?.endsWith("psmux.mjs")) {
1234
1296
  (async () => {
1235
1297
  const [, , cmd, ...args] = process.argv;
1236
1298
 
@@ -14,7 +14,7 @@ import { detectInputWait, PROBE_DEFAULTS } from './health-probe.mjs';
14
14
  const COMPLETION_TOKEN_RE = /__TRIFLUX_DONE__/;
15
15
 
16
16
  /** 프롬프트 idle 패턴 (Claude Code 프롬프트 복귀) */
17
- const PROMPT_IDLE_RE = /(\u276f|\u2795|>\s*$)/;
17
+ const _PROMPT_IDLE_RE = /(\u276f|\u2795|>\s*$)/;
18
18
 
19
19
  /**
20
20
  * SSH 경유로 원격 psmux capture-pane 실행.
@@ -5,6 +5,7 @@
5
5
  import { execFileSync } from 'node:child_process';
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
7
  import { basename, join, posix as posixPath, win32 as win32Path } from 'node:path';
8
+ import { execSshWithRetry } from '@triflux/core/hub/lib/ssh-retry.mjs';
8
9
 
9
10
  const REMOTE_ENV_TTL_MS = 86_400_000; // 24h
10
11
  const REMOTE_STAGE_ROOT = 'tfx-remote';
@@ -84,8 +85,9 @@ function probeRemoteEnvViaPwsh(host) {
84
85
  ].join('; ');
85
86
 
86
87
  try {
87
- const output = execFileSync('ssh', [host, 'pwsh', '-NoProfile', '-Command', command], {
88
+ const output = execSshWithRetry([host, 'pwsh', '-NoProfile', '-Command', command], {
88
89
  encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'],
90
+ maxRetries: 2, baseDelayMs: 1000,
89
91
  });
90
92
  return normalizePwshProbeEnv(parseProbeLines(output));
91
93
  } catch {
@@ -102,8 +104,9 @@ function probeRemoteEnvViaPosix(host) {
102
104
  ].join('\n');
103
105
 
104
106
  try {
105
- const output = execFileSync('ssh', [host, 'sh'], {
107
+ const output = execSshWithRetry([host, 'sh'], {
106
108
  encoding: 'utf8', timeout: 15000, input: script,
109
+ maxRetries: 2, baseDelayMs: 1000,
107
110
  });
108
111
  return normalizePosixProbeEnv(parseProbeLines(output));
109
112
  } catch {
@@ -40,7 +40,7 @@ const PROMPT_IDLE_PATTERNS = Object.freeze([
40
40
  ]);
41
41
 
42
42
  /** @deprecated shellQuoteForHost(value, os) 사용 권장 — OS-aware 쿼팅 */
43
- function shellQuote(value, os) {
43
+ function _shellQuote(value, os) {
44
44
  if (os) return shellQuoteForHost(value, os);
45
45
  return `'${String(value).replace(/'/g, "'\\''")}'`;
46
46
  }
@@ -481,7 +481,7 @@ export function configureTeammateKeybindings(sessionName, opts = {}) {
481
481
  if (taskListCommand) {
482
482
  const escaped = taskListCommand.replace(/'/g, "'\\''");
483
483
  try {
484
- tmux(`bind-key -T root -n C-t if-shell -F '${cond}' \"display-popup -E '${escaped}'\" \"send-keys C-t\"`);
484
+ tmux(`bind-key -T root -n C-t if-shell -F '${cond}' "display-popup -E '${escaped}'" "send-keys C-t"`);
485
485
  } catch {
486
486
  tmux(`bind-key -T root -n C-t if-shell -F '${cond}' 'display-message "tfx multi tasks 명령으로 태스크 확인"' 'send-keys C-t'`);
487
487
  }