@triflux/remote 10.74.0 → 10.76.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.
@@ -46,7 +46,8 @@ export function parseTeamArgs(args = []) {
46
46
  let teammateMode = "auto";
47
47
  const taskParts = [];
48
48
  const assigns = [];
49
- let autoAttach = true;
49
+ // null 이면 start-headless 가 플랫폼으로 정한다: Windows 만 켬 (ADR-0040)
50
+ let autoAttach = null;
50
51
  let progressive = true;
51
52
  let timeoutSec = 0;
52
53
  let verbose = false;
@@ -82,6 +83,8 @@ export function parseTeamArgs(args = []) {
82
83
  } else if (current === "--assign" && args[index + 1]) {
83
84
  const parsed = parseAssignValue(args[++index]);
84
85
  if (parsed) assigns.push(parsed);
86
+ } else if (current === "--auto-attach") {
87
+ autoAttach = true;
85
88
  } else if (current === "--no-auto-attach") {
86
89
  autoAttach = false;
87
90
  } else if (current === "--verbose") {
@@ -143,7 +146,7 @@ export function parseTeamArgs(args = []) {
143
146
  p = p[1].toUpperCase() + ":" + p.slice(2);
144
147
  }
145
148
  cwd = resolve(p);
146
- } else if (current === "--auto-attach" || current === "--dashboard") {
149
+ } else if (current === "--dashboard") {
147
150
  // 이전 설치 스킬이 넘기는 기본값 플래그는 무시한다.
148
151
  } else if (current.startsWith("-")) {
149
152
  console.warn(` ⚠ 미인식 플래그 무시: ${current}`);
@@ -107,13 +107,23 @@ export async function startHeadlessTeam({
107
107
  );
108
108
  }
109
109
 
110
+ // macOS/Linux 는 claude agents 행이 관전 창이라 호출자 tmux 창을 나누지 않는다.
111
+ // Windows 는 그 행이 없어 WT 탭 attach 를 켠다 (ADR-0040).
112
+ const platform = _deps.platform || process.platform;
113
+ const effectiveAutoAttach = autoAttach ?? platform === "win32";
114
+ if (effectiveAutoAttach && platform !== "win32" && process.env.TMUX_PANE) {
115
+ ok(
116
+ "--auto-attach: 호출자 tmux 창을 아래로 30% 나눠 워커 방에 attach 함 (끄기: --no-auto-attach)",
117
+ );
118
+ }
119
+
110
120
  let handle;
111
121
  try {
112
122
  const run = _deps.runHeadlessInteractive || runHeadlessInteractive;
113
123
  handle = await run(sessionId, assignments, {
114
124
  timeoutSec: timeoutSec || 900,
115
125
  layout,
116
- autoAttach: !!autoAttach,
126
+ autoAttach: effectiveAutoAttach,
117
127
  leadPane: process.env.TMUX_PANE,
118
128
  leadTmux: process.env.TMUX,
119
129
  dashboard: !!dashboard,
@@ -9,6 +9,7 @@ export function renderTeamHelp() {
9
9
  ${WHITE}tfx multi --agents codex,antigravity --lead claude "작업"${RESET}
10
10
  ${WHITE}tfx multi --teammate-mode tmux "작업"${RESET}
11
11
  ${WHITE}tfx multi --teammate-mode headless "작업"${RESET}
12
+ ${WHITE}tfx multi --auto-attach "작업"${RESET} ${DIM}(headless: 호출자 tmux 창을 나눠 attach. Windows 는 기본 켬)${RESET}
12
13
  ${WHITE}tfx multi --layout 1xN "작업"${RESET} ${DIM}(세로 분할 컬럼)${RESET}
13
14
  ${WHITE}tfx multi --layout Nx1 "작업"${RESET} ${DIM}(가로 분할 스택)${RESET}
14
15
  ${WHITE}tfx multi --dashboard-layout lite "작업"${RESET} ${DIM}(dashboard-lite 기본 뷰)${RESET}
@@ -13,12 +13,14 @@ export function normalizeTeammateMode(mode = "auto", deps = {}) {
13
13
  `지원하지 않는 teammate mode: ${mode}; auto, headless, tmux${platform === "win32" ? ", psmux" : ""}를 사용하세요.`,
14
14
  );
15
15
  }
16
+ // 비TTY 호출자(파이프, Claude Code Bash 도구, CI)는 대화형 팀에 입력할 수 없고
17
+ // 결과 색인도 headless 에서만 나온다. tmux 안이라도 headless 로 간다 (ADR-0040).
18
+ if (!(deps.isTTY ?? Boolean(process.stdout.isTTY))) return "headless";
16
19
  if (env.TMUX) return "tmux";
17
20
  const mux = detectMux();
18
21
  if (mux === "tmux") return "tmux";
19
22
  if (platform === "win32" && mux === "psmux") return "psmux";
20
23
  if (platform === "win32" && mux === "git-bash-tmux") return "tmux";
21
- if (!(deps.isTTY ?? Boolean(process.stdout.isTTY))) return "headless";
22
24
  return ensureTmuxOrExit(deps);
23
25
  }
24
26
 
@@ -997,11 +997,18 @@ function killOrphanMcpProcesses(sessionName) {
997
997
  function detachAttachedClients(sessionName, waitMs = 750) {
998
998
  const attachedCount = getPsmuxSessionAttachedCount(sessionName);
999
999
  if (!Number.isFinite(attachedCount) || attachedCount <= 0) return false;
1000
+ // -t 는 클라이언트 이름이다. 세션에 붙은 클라이언트 전부를 떼려면 -s 로 세션을 주고,
1001
+ // tmux 의 접두사 매칭이 같은 접두사의 다른 세션을 떼지 않게 정확한 타깃을 쓴다.
1000
1002
  try {
1001
- psmuxExec(["detach-client", "-t", sessionName], { stdio: "ignore" });
1003
+ psmuxExec(["detach-client", "-s", exactSessionTarget(sessionName)], {
1004
+ stdio: "ignore",
1005
+ });
1002
1006
  sleepMs(waitMs);
1003
1007
  return true;
1004
- } catch {
1008
+ } catch (error) {
1009
+ process.stderr.write(
1010
+ `[psmux] detach-client 실패 (${sessionName}): ${error?.message || error}\n`,
1011
+ );
1005
1012
  return false;
1006
1013
  }
1007
1014
  }
@@ -1,8 +1,15 @@
1
1
  // Remote session primitives.
2
2
  // Pure functions + SSH operations. No psmux, no WT, no CLI arg parsing.
3
3
 
4
- import { execFileSync } from "node:child_process";
5
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { execFileSync, execSync } from "node:child_process";
5
+ import {
6
+ chmodSync,
7
+ existsSync,
8
+ lstatSync,
9
+ mkdirSync,
10
+ readFileSync,
11
+ writeFileSync,
12
+ } from "node:fs";
6
13
  import { homedir } from "node:os";
7
14
  import {
8
15
  basename,
@@ -17,6 +24,45 @@ const REMOTE_ENV_TTL_MS = 86_400_000; // 24h
17
24
  const REMOTE_STAGE_ROOT = "tfx-remote";
18
25
  const SAFE_HOST_RE = /^[a-zA-Z0-9._-]+$/;
19
26
 
27
+ let sshControlDir = null;
28
+ if (process.platform !== "win32" && typeof process.getuid === "function") {
29
+ // macOS Unix 소켓 경로의 104바이트 제한 때문에 $TMPDIR 대신 /tmp를 쓴다.
30
+ const dir = `/tmp/tfx-cm-${process.getuid()}`;
31
+ try {
32
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
33
+ const stat = lstatSync(dir);
34
+ if (stat.isDirectory() && stat.uid === process.getuid()) {
35
+ chmodSync(dir, 0o700);
36
+ sshControlDir = dir;
37
+ }
38
+ } catch {}
39
+ }
40
+
41
+ export function buildSshOptions({
42
+ interactive = false,
43
+ platform = process.platform,
44
+ } = {}) {
45
+ const options = [
46
+ "-o",
47
+ "ServerAliveInterval=15",
48
+ "-o",
49
+ "ServerAliveCountMax=4",
50
+ ];
51
+ if (!interactive)
52
+ options.push("-o", "BatchMode=yes", "-o", "ConnectTimeout=10");
53
+ if (platform !== "win32" && sshControlDir) {
54
+ options.push(
55
+ "-o",
56
+ "ControlMaster=auto",
57
+ "-o",
58
+ `ControlPath=${sshControlDir}/%C`,
59
+ "-o",
60
+ "ControlPersist=60",
61
+ );
62
+ }
63
+ return options;
64
+ }
65
+
20
66
  // ── Shell quoting utilities ─────────────────────────────────────
21
67
 
22
68
  export function shellQuote(value) {
@@ -76,19 +122,107 @@ function parseProbeLines(text) {
76
122
  function normalizePwshProbeEnv(parsed) {
77
123
  if (parsed.shell !== "pwsh" || parsed.os !== "win32") return null;
78
124
  if (!parsed.home) return null;
125
+ const psmuxVersion = parsed.psmuxVersion || null;
126
+ const version = psmuxVersion && parseVersion(psmuxVersion);
79
127
  return Object.freeze({
80
128
  claudePath:
81
129
  !parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
130
+ psmuxPath:
131
+ !parsed.psmux || parsed.psmux === "notfound" ? null : parsed.psmux,
132
+ psmuxVersion,
133
+ // tfx-psmux.md 규칙 5의 3.3.x detach 미지원 범위를 제외한다. 3.4.0은 일반 이름도 정확 매칭한다.
134
+ psmuxSupported: Boolean(
135
+ parsed.psmux &&
136
+ parsed.psmux !== "notfound" &&
137
+ version &&
138
+ compareVersions(version, [3, 4, 0]) >= 0,
139
+ ),
82
140
  home: parsed.home,
83
141
  os: "win32",
84
142
  shell: "pwsh",
85
143
  });
86
144
  }
87
145
 
88
- function normalizePosixProbeEnv(parsed) {
146
+ function parseVersion(versionStr) {
147
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(versionStr);
148
+ if (!match) return null;
149
+ return [
150
+ parseInt(match[1], 10),
151
+ parseInt(match[2], 10),
152
+ parseInt(match[3], 10),
153
+ ];
154
+ }
155
+
156
+ export function compareVersions(a, b) {
157
+ for (let i = 0; i < 3; i++) {
158
+ if (a[i] !== b[i]) return a[i] - b[i];
159
+ }
160
+ return 0;
161
+ }
162
+
163
+ export function probeVersion(binPath) {
164
+ try {
165
+ if (/\.(cmd|bat)$/iu.test(binPath)) {
166
+ // .cmd/.bat → execSync로 shell 경유 (execFileSync EINVAL 회피)
167
+ const out = execSync(`"${binPath}" --version`, {
168
+ encoding: "utf8",
169
+ timeout: 3000,
170
+ stdio: ["ignore", "pipe", "ignore"],
171
+ });
172
+ return parseVersion(out);
173
+ }
174
+ const out = execFileSync(binPath, ["--version"], {
175
+ encoding: "utf8",
176
+ timeout: 3000,
177
+ stdio: ["ignore", "pipe", "ignore"],
178
+ });
179
+ return parseVersion(out);
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+
185
+ export function normalizePosixProbeEnv(parsed, localVersions = null) {
89
186
  const os =
90
187
  parsed.os === "darwin" ? "darwin" : parsed.os === "linux" ? "linux" : null;
91
- if (!os || !parsed.home) return null;
188
+ if (!os || !parsed.home) {
189
+ return null;
190
+ }
191
+
192
+ const versions = Object.fromEntries(
193
+ ["node", "codex", "agy", "claude", "triflux"].map((name) => [
194
+ name,
195
+ parsed[`${name}Version`] || null,
196
+ ]),
197
+ );
198
+ const warnings = [];
199
+ const memoryPressureLevel =
200
+ os === "darwin" ? Number(parsed.memoryPressureLevel) || null : null;
201
+ const memoryFreePct =
202
+ parsed.memoryFreePct && Number.isFinite(Number(parsed.memoryFreePct))
203
+ ? Number(parsed.memoryFreePct)
204
+ : null;
205
+ const loadAvgValues = parsed.loadAvg?.split(",").map(Number);
206
+ const loadAvg =
207
+ loadAvgValues?.length === 3 && loadAvgValues.every(Number.isFinite)
208
+ ? loadAvgValues
209
+ : null;
210
+ const diskFreeHome = parsed.diskFreeHome ? Number(parsed.diskFreeHome) : null;
211
+
212
+ if (memoryPressureLevel >= 2) {
213
+ warnings.push(`memory pressure level ${memoryPressureLevel}`);
214
+ }
215
+ if (diskFreeHome !== null && diskFreeHome < 5 * 1024 ** 3) {
216
+ warnings.push("home disk free below 5 GiB");
217
+ }
218
+ for (const [name, remote] of Object.entries(versions)) {
219
+ const local = localVersions?.[name];
220
+ const remoteVersion = remote && parseVersion(remote);
221
+ if (local && remoteVersion && compareVersions(local, remoteVersion) !== 0) {
222
+ warnings.push(`${name} version differs`);
223
+ }
224
+ }
225
+
92
226
  return Object.freeze({
93
227
  claudePath:
94
228
  !parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
@@ -96,26 +230,36 @@ function normalizePosixProbeEnv(parsed) {
96
230
  !parsed.codex || parsed.codex === "notfound" ? null : parsed.codex,
97
231
  geminiPath:
98
232
  !parsed.gemini || parsed.gemini === "notfound" ? null : parsed.gemini,
233
+ tmuxPath: !parsed.tmux || parsed.tmux === "notfound" ? null : parsed.tmux,
234
+ node: parsed.node || parsed.nodeVersion || null,
235
+ cores: parsed.cores ? Number(parsed.cores) : null,
236
+ ramGb: parsed.ram_gb ? Number(parsed.ram_gb) : null,
99
237
  home: parsed.home,
238
+ codexAuthExists: parsed.codexAuthExists === "true",
239
+ diskFreeHome,
240
+ loadAvg,
241
+ memoryFreePct,
242
+ memoryPressureLevel,
100
243
  os,
244
+ ready: memoryPressureLevel !== 4,
101
245
  shell: parsed.shell === "zsh" ? "zsh" : "bash",
102
- node: parsed.node || null,
103
- cores: parsed.cores ? Number(parsed.cores) : null,
104
- ramGb: parsed.ram_gb ? Number(parsed.ram_gb) : null,
246
+ versions,
247
+ warnings,
105
248
  });
106
249
  }
107
250
 
108
- function probeRemoteEnvViaPwsh(host) {
251
+ function probeRemoteEnvViaPwsh(host, execSsh) {
109
252
  const command = [
110
253
  "Write-Output 'shell=pwsh'",
111
254
  'Write-Output "home=$env:USERPROFILE"',
112
255
  'if (Test-Path "$env:USERPROFILE\\.local\\bin\\claude.exe") { Write-Output "claude=$env:USERPROFILE\\.local\\bin\\claude.exe" } elseif (Get-Command claude -ErrorAction SilentlyContinue) { Write-Output "claude=$((Get-Command claude).Source)" } else { Write-Output \'claude=notfound\' }',
256
+ 'if (Get-Command psmux -ErrorAction SilentlyContinue) { Write-Output "psmux=$((Get-Command psmux).Source)"; $psmuxVersion = psmux -V 2>$null; if ($LASTEXITCODE -eq 0) { Write-Output "psmuxVersion=$psmuxVersion" } } else { Write-Output \'psmux=notfound\' }',
113
257
  "Write-Output \"os=$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) ? 'win32' : 'other')\"",
114
258
  ].join("; ");
115
259
 
116
260
  try {
117
- const output = execSshWithRetry(
118
- [host, "pwsh", "-NoProfile", "-Command", command],
261
+ const output = execSsh(
262
+ [...buildSshOptions(), host, pwshRemoteCommand(command)],
119
263
  {
120
264
  encoding: "utf8",
121
265
  timeout: 15000,
@@ -130,7 +274,7 @@ function probeRemoteEnvViaPwsh(host) {
130
274
  }
131
275
  }
132
276
 
133
- function probeRemoteEnvViaPosix(host) {
277
+ function probeRemoteEnvViaPosix(host, execSsh, localVersions) {
134
278
  const script = [
135
279
  '[ -f "$HOME/.zshenv" ] && . "$HOME/.zshenv" 2>/dev/null || true',
136
280
  "export PATH=/opt/homebrew/bin:/opt/homebrew/sbin:$HOME/.local/bin:$HOME/.nvm/versions/node/$(ls $HOME/.nvm/versions/node 2>/dev/null | sort -V | tail -1)/bin:$PATH 2>/dev/null",
@@ -140,22 +284,40 @@ function probeRemoteEnvViaPosix(host) {
140
284
  "command -v codex >/dev/null 2>&1 && echo codex=$(command -v codex) || echo codex=notfound",
141
285
  "command -v gemini >/dev/null 2>&1 && echo gemini=$(command -v gemini) || echo gemini=notfound",
142
286
  "echo os=$(uname -s | tr A-Z a-z)",
143
- "node --version 2>/dev/null && echo node=$(node --version) || echo node=notfound",
144
287
  // darwin: sysctl, linux: nproc + /proc/meminfo
145
288
  "if [ $(uname -s) = Darwin ]; then echo cores=$(sysctl -n hw.ncpu); echo ram_gb=$(($(sysctl -n hw.memsize) / 1073741824)); else echo cores=$(nproc 2>/dev/null || echo 0); echo ram_gb=$(($(grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}') / 1048576)); fi",
289
+ "command -v tmux >/dev/null 2>&1 && echo tmux=$(command -v tmux) || echo tmux=notfound",
290
+ 'if [ "$(uname -s)" = Darwin ]; then',
291
+ " printf 'memoryPressureLevel=%s\\n' \"$(sysctl -n kern.memorystatus_vm_pressure_level 2>/dev/null)\"",
292
+ ' vm_stat | awk -v total="$(sysctl -n hw.memsize 2>/dev/null)" -v pageSize="$(sysctl -n hw.pagesize 2>/dev/null)" \'/Pages free:|Pages inactive:|Pages speculative:/ {gsub(/[^0-9]/, "", $3); pages += $3} END {if (total > 0 && pageSize > 0) printf "memoryFreePct=%.1f\\n", pages * pageSize / total * 100}\'',
293
+ ' sysctl -n vm.loadavg 2>/dev/null | tr -d \'{}\' | awk \'{print "loadAvg=" $1 "," $2 "," $3}\'',
294
+ "else",
295
+ " awk '/MemTotal:/ {total=$2} /MemAvailable:/ {available=$2} END {if (total > 0) printf \"memoryFreePct=%.1f\\n\", available / total * 100}' /proc/meminfo 2>/dev/null",
296
+ ' awk \'{print "loadAvg=" $1 "," $2 "," $3}\' /proc/loadavg 2>/dev/null',
297
+ "fi",
298
+ 'df -Pk "$HOME" 2>/dev/null | awk \'NR==2 {printf "diskFreeHome=%.0f\\n", $4 * 1024}\'',
299
+ 'if [ -f "${CODEX_HOME:-$HOME/.codex}/auth.json" ]; then echo codexAuthExists=true; else echo codexAuthExists=false; fi',
300
+ ...(localVersions
301
+ ? [
302
+ 'probe_version() { command -v "$1" >/dev/null 2>&1 && "$1" --version </dev/null 2>/dev/null | head -n 1; }',
303
+ 'for cli in node codex agy claude triflux; do printf \'%sVersion=%s\\n\' "$cli" "$(probe_version "$cli")"; done',
304
+ ]
305
+ : []),
146
306
  ].join("\n");
147
307
 
148
308
  try {
149
- const output = execSshWithRetry([host, "sh"], {
309
+ const output = execSsh([...buildSshOptions(), host, "sh"], {
150
310
  encoding: "utf8",
151
311
  timeout: 15000,
152
312
  input: script,
153
313
  maxRetries: 2,
154
314
  baseDelayMs: 1000,
155
315
  });
156
- return normalizePosixProbeEnv(parseProbeLines(output));
157
- } catch {
158
- return null;
316
+ return normalizePosixProbeEnv(parseProbeLines(output), localVersions);
317
+ } catch (error) {
318
+ throw new Error(`remote probe failed for ${host}: ${error.message}`, {
319
+ cause: error,
320
+ });
159
321
  }
160
322
  }
161
323
 
@@ -223,22 +385,35 @@ function writeEnvCache(host, env, cacheDir) {
223
385
  export function probeRemoteEnv(host, opts = {}) {
224
386
  validateHost(host);
225
387
  const force = opts.force === true;
388
+ const execSsh = opts.execSsh || execSshWithRetry;
226
389
  const cacheDir = opts.cacheDir || remoteEnvCacheDir();
227
390
 
228
391
  if (!force) {
229
392
  const cached = readEnvCache(host, cacheDir);
230
- if (isEnvCacheFresh(cached)) return cached.env;
393
+ if (
394
+ isEnvCacheFresh(cached) &&
395
+ cached.env.claudePath &&
396
+ (!opts.osHint ||
397
+ cached.env.os === opts.osHint ||
398
+ (opts.osHint === "windows" && cached.env.os === "win32")) &&
399
+ (cached.env.os === "win32"
400
+ ? "psmuxVersion" in cached.env && "psmuxSupported" in cached.env
401
+ : "tmuxPath" in cached.env && "versions" in cached.env)
402
+ )
403
+ return cached.env;
231
404
  }
232
405
 
233
- const pwshEnv = probeRemoteEnvViaPwsh(host);
406
+ const pwshEnv = ["darwin", "linux"].includes(opts.osHint)
407
+ ? null
408
+ : probeRemoteEnvViaPwsh(host, execSsh);
234
409
  if (pwshEnv) {
235
- writeEnvCache(host, pwshEnv, cacheDir);
410
+ if (pwshEnv.claudePath) writeEnvCache(host, pwshEnv, cacheDir);
236
411
  return pwshEnv;
237
412
  }
238
413
 
239
- const posixEnv = probeRemoteEnvViaPosix(host);
414
+ const posixEnv = probeRemoteEnvViaPosix(host, execSsh, opts.localVersions);
240
415
  if (posixEnv) {
241
- writeEnvCache(host, posixEnv, cacheDir);
416
+ if (posixEnv.claudePath) writeEnvCache(host, posixEnv, cacheDir);
242
417
  return posixEnv;
243
418
  }
244
419
 
@@ -303,7 +478,10 @@ export function ensureRemoteStageDir(host, env, remoteStageDir) {
303
478
  `New-Item -ItemType Directory -Path '${escapePwshSingleQuoted(remoteStageDir)}' -Force | Out-Null`,
304
479
  )
305
480
  : posixRemoteCommand(`mkdir -p ${shellQuote(remoteStageDir)}`);
306
- execFileSync("ssh", [host, command], { timeout: 10000, stdio: "pipe" });
481
+ execFileSync("ssh", [...buildSshOptions(), host, command], {
482
+ timeout: 10000,
483
+ stdio: "pipe",
484
+ });
307
485
  }
308
486
 
309
487
  /**
@@ -313,10 +491,14 @@ export function ensureRemoteStageDir(host, env, remoteStageDir) {
313
491
  * @param {string} remotePath
314
492
  */
315
493
  export function uploadFileToRemote(host, localPath, remotePath) {
316
- execFileSync("scp", [localPath, `${host}:${remotePath}`], {
317
- timeout: 15000,
318
- stdio: "pipe",
319
- });
494
+ execFileSync(
495
+ "scp",
496
+ [...buildSshOptions(), localPath, `${host}:${remotePath}`],
497
+ {
498
+ timeout: 15000,
499
+ stdio: "pipe",
500
+ },
501
+ );
320
502
  }
321
503
 
322
504
  /**
@@ -372,7 +554,7 @@ export function remoteGit(host, env, gitArgs, cwd) {
372
554
  : posixRemoteCommand(
373
555
  `cd ${shellQuote(cwd)} && ${["git", ...gitArgs].map(shellQuote).join(" ")}`,
374
556
  );
375
- return execFileSync("ssh", [host, command], {
557
+ return execFileSync("ssh", [...buildSshOptions(), host, command], {
376
558
  encoding: "utf8",
377
559
  timeout: 30_000,
378
560
  stdio: ["pipe", "pipe", "pipe"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.74.0",
3
+ "version": "10.76.0",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",