triflux 8.0.0 → 8.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "8.0.0",
3
+ "version": "8.2.2",
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": {
@@ -10,8 +10,8 @@
10
10
  // node remote-spawn.mjs --probe <ssh-host>
11
11
 
12
12
  import { randomUUID } from "crypto";
13
- import { execFileSync, spawn } from "child_process";
14
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
13
+ import { execFileSync, execSync, spawn } from "child_process";
14
+ import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
15
15
  import { homedir, platform as getPlatform, tmpdir } from "os";
16
16
  import { join, posix as posixPath, resolve, win32 as win32Path } from "path";
17
17
  import {
@@ -184,24 +184,76 @@ function parseArgs(argv) {
184
184
  };
185
185
  }
186
186
 
187
+ function parseVersion(versionStr) {
188
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(versionStr);
189
+ if (!match) return null;
190
+ return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
191
+ }
192
+
193
+ function compareVersions(a, b) {
194
+ for (let i = 0; i < 3; i++) {
195
+ if (a[i] !== b[i]) return a[i] - b[i];
196
+ }
197
+ return 0;
198
+ }
199
+
200
+ function probeVersion(binPath) {
201
+ try {
202
+ if (/\.(cmd|bat)$/iu.test(binPath)) {
203
+ // .cmd/.bat → execSync로 shell 경유 (execFileSync EINVAL 회피)
204
+ const out = execSync(`"${binPath}" --version`, {
205
+ encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
206
+ });
207
+ return parseVersion(out);
208
+ }
209
+ const out = execFileSync(binPath, ["--version"], {
210
+ encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
211
+ });
212
+ return parseVersion(out);
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+
187
218
  function detectClaudePath() {
188
219
  if (process.env.CLAUDE_BIN_PATH) return process.env.CLAUDE_BIN_PATH;
189
220
 
221
+ const candidates = [];
222
+
190
223
  const wingetPath = join(homedir(), "AppData", "Local", "Microsoft", "WinGet", "Links", "claude.exe");
191
- if (existsSync(wingetPath)) return wingetPath;
224
+ if (existsSync(wingetPath)) candidates.push(wingetPath);
192
225
 
193
226
  const npmPath = join(process.env.APPDATA || "", "npm", "claude.cmd");
194
- if (existsSync(npmPath)) return npmPath;
227
+ if (existsSync(npmPath)) candidates.push(npmPath);
195
228
 
196
229
  try {
197
230
  const command = IS_WINDOWS_LOCAL ? "where" : "which";
198
231
  const result = execFileSync(command, ["claude"], { encoding: "utf8", timeout: 5000 }).trim();
199
- if (result) return result.split(/\r?\n/u)[0].trim();
232
+ if (result) {
233
+ for (const line of result.split(/\r?\n/u)) {
234
+ const p = line.trim();
235
+ if (p && !candidates.includes(p)) candidates.push(p);
236
+ }
237
+ }
200
238
  } catch {
201
239
  // not found
202
240
  }
203
241
 
204
- return "claude";
242
+ if (candidates.length === 0) return "claude";
243
+
244
+ let bestPath = candidates[0];
245
+ let bestVersion = probeVersion(candidates[0]);
246
+
247
+ for (const candidate of candidates.slice(1)) {
248
+ const ver = probeVersion(candidate);
249
+ if (ver === null) continue;
250
+ if (bestVersion === null || compareVersions(ver, bestVersion) > 0) {
251
+ bestVersion = ver;
252
+ bestPath = candidate;
253
+ }
254
+ }
255
+
256
+ return bestPath;
205
257
  }
206
258
 
207
259
  function getPermissionFlag() {
@@ -598,16 +650,44 @@ function spawnLocal(args, claudePath, prompt) {
598
650
  const sessionName = `tfx-spawn-${randomUUID().slice(0, 8)}`;
599
651
  const paneId = `${sessionName}:0.0`;
600
652
  const permissionFlags = getPermissionFlag().join(" ");
601
- const command = `& '${escapePwshSingleQuoted(normalizeCommandPath(claudePath))}'${permissionFlags ? ` ${permissionFlags}` : ""}`;
653
+ const claudePathNorm = normalizeCommandPath(claudePath);
654
+
655
+ // 임시파일 생성 (프롬프트가 있을 때만)
656
+ // 정리는 pwsh 스크립트 내부에서 수행 (Node exit 시 삭제하면 pane 실행 전 사라짐)
657
+ let tmpFile = null;
658
+ if (prompt) {
659
+ tmpFile = join(tmpdir(), `tfx-prompt-${randomUUID().slice(0, 8)}.md`);
660
+ writeFileSync(tmpFile, prompt, { encoding: "utf8" });
661
+ }
602
662
 
603
663
  createPsmuxSession(sessionName, { layout: "1xN", paneCount: 1 });
604
664
  sendKeysToPane(paneId, `cd '${escapePwshSingleQuoted(dir)}'`);
605
- sleepMs(500);
606
- sendKeysToPane(paneId, command);
607
- if (prompt) {
608
- sleepMs(2000);
609
- sendKeysToPane(paneId, prompt);
665
+ sleepMs(300);
666
+
667
+ if (prompt && tmpFile) {
668
+ // pwsh -File 패턴: 인라인 쿼팅 문제 회피 (피드백: -Command 금지)
669
+ // 1단계: 프롬프트를 Get-Content -Raw → claude -p (one-shot), 세션 ID 추출
670
+ // 2단계: --resume으로 인터랙티브 세션 이어붙이기
671
+ const tmpFileNorm = normalizeCommandPath(tmpFile);
672
+ const flags = getPermissionFlag().map((f) => `'${escapePwshSingleQuoted(f)}'`).join(", ");
673
+ const scriptContent = [
674
+ `$ErrorActionPreference = 'SilentlyContinue'`,
675
+ `$t = '${escapePwshSingleQuoted(tmpFileNorm)}'`,
676
+ `$c = '${escapePwshSingleQuoted(claudePathNorm)}'`,
677
+ `$f = @(${flags})`,
678
+ `$raw = Get-Content -Raw $t`,
679
+ `Remove-Item -ErrorAction SilentlyContinue $t`,
680
+ `Remove-Item -ErrorAction SilentlyContinue $MyInvocation.MyCommand.Definition`,
681
+ `& $c @f $raw`,
682
+ ].join("\n");
683
+ const scriptFile = join(tmpdir(), `tfx-spawn-${randomUUID().slice(0, 8)}.ps1`);
684
+ writeFileSync(scriptFile, scriptContent, { encoding: "utf8" });
685
+ sendKeysToPane(paneId, `pwsh -NoProfile -NoExit -File '${escapePwshSingleQuoted(normalizeCommandPath(scriptFile))}'`);
686
+ } else {
687
+ const command = `& '${escapePwshSingleQuoted(claudePathNorm)}'${permissionFlags ? ` ${permissionFlags}` : ""}`;
688
+ sendKeysToPane(paneId, command);
610
689
  }
690
+
611
691
  openAttachTab(sessionName, "Claude@local");
612
692
  console.log(sessionName);
613
693
  }
@@ -29,6 +29,15 @@ VERSION="2.5"
29
29
 
30
30
  set -euo pipefail
31
31
 
32
+ # ── timeout 명령 호환성 — Windows에서 TIMEOUT.exe 대신 Git Bash coreutils timeout 사용 ──
33
+ if command -v /usr/bin/timeout >/dev/null 2>&1; then
34
+ TIMEOUT_BIN="/usr/bin/timeout"
35
+ elif command -v gtimeout >/dev/null 2>&1; then
36
+ TIMEOUT_BIN="gtimeout" # macOS homebrew
37
+ else
38
+ TIMEOUT_BIN="timeout" # Linux 기본
39
+ fi
40
+
32
41
  # ── Async Job 디렉토리 ──
33
42
  TFX_JOBS_DIR="${TMPDIR:-/tmp}/tfx-jobs"
34
43
 
@@ -1120,9 +1129,9 @@ run_stream_worker() {
1120
1129
  )
1121
1130
 
1122
1131
  if [[ "$use_tee_flag" == "true" ]]; then
1123
- printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
1132
+ printf '%s' "$prompt" | "$TIMEOUT_BIN" "$TIMEOUT_SEC" "${worker_cmd[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
1124
1133
  else
1125
- printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
1134
+ printf '%s' "$prompt" | "$TIMEOUT_BIN" "$TIMEOUT_SEC" "${worker_cmd[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
1126
1135
  fi
1127
1136
  worker_pid=$!
1128
1137
 
@@ -1144,9 +1153,9 @@ _gemini_run_once() {
1144
1153
  local -a g_args=("$@")
1145
1154
 
1146
1155
  if [[ "$use_tee_flag" == "true" ]]; then
1147
- timeout "$TIMEOUT_SEC" "$CLI_CMD" "${g_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
1156
+ "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${g_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
1148
1157
  else
1149
- timeout "$TIMEOUT_SEC" "$CLI_CMD" "${g_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
1158
+ "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${g_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
1150
1159
  fi
1151
1160
  echo "$!"
1152
1161
  }
@@ -1285,9 +1294,9 @@ run_codex_exec() {
1285
1294
  fi
1286
1295
 
1287
1296
  if [[ "$use_tee_flag" == "true" ]]; then
1288
- timeout "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
1297
+ "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
1289
1298
  else
1290
- timeout "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
1299
+ "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
1291
1300
  fi
1292
1301
  worker_pid=$!
1293
1302
 
@@ -1379,9 +1388,9 @@ run_codex_mcp() {
1379
1388
  esac
1380
1389
 
1381
1390
  if [[ "$use_tee_flag" == "true" ]]; then
1382
- timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
1391
+ "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
1383
1392
  else
1384
- timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
1393
+ "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
1385
1394
  fi
1386
1395
  worker_pid=$!
1387
1396