triflux 10.18.2 → 10.20.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.
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
+ "name": "triflux",
4
+ "description": "CLI-first multi-model orchestrator — Codex/Gemini/Claude routing with DAG execution, auto-triage, and cost optimization",
5
+ "owner": {
6
+ "name": "tellang"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "triflux",
11
+ "description": "Tri-CLI orchestrator for Claude Code. Routes tasks across Claude + Codex + Gemini with consensus intelligence, natural language routing, 42 skills, and cross-model review.",
12
+ "version": "10.20.0",
13
+ "author": {
14
+ "name": "tellang"
15
+ },
16
+ "source": {
17
+ "source": "npm",
18
+ "package": "triflux"
19
+ },
20
+ "category": "productivity",
21
+ "homepage": "https://github.com/tellang/triflux",
22
+ "tags": [
23
+ "multi-model",
24
+ "codex",
25
+ "gemini",
26
+ "cli-routing",
27
+ "orchestration",
28
+ "cost-optimization",
29
+ "dag-execution"
30
+ ]
31
+ }
32
+ ],
33
+ "version": "10.20.0"
34
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "triflux",
3
+ "version": "10.20.0",
4
+ "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
+ "author": {
6
+ "name": "tellang"
7
+ },
8
+ "repository": "https://github.com/tellang/triflux",
9
+ "homepage": "https://github.com/tellang/triflux",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "claude-code",
13
+ "plugin",
14
+ "codex",
15
+ "gemini",
16
+ "cli-routing",
17
+ "orchestration",
18
+ "multi-model"
19
+ ],
20
+ "skills": "./skills/",
21
+ "hooks": "./hooks/hooks.json"
22
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "mcp-registry-schema",
3
+ "version": 1,
4
+ "description": "MCP 서버 중앙 레지스트리 — 진실의 원천",
5
+ "defaults": {
6
+ "transport": "hub-url",
7
+ "hub_base": "http://127.0.0.1:27888"
8
+ },
9
+ "policy_notes": {
10
+ "transport": "Server transport accepts \"hub-url\" for the existing triflux Hub URL flow or \"http\" for direct Streamable HTTP MCP endpoints. Direct stdio registration via command/args is intentionally unsupported.",
11
+ "headers": "Optional headers are allowed only for HTTP-compatible transports. Each header value must be a descriptor: {\"value\":\"literal\"} for non-secret static values, {\"env\":\"ENV_VAR_NAME\"} for secrets resolved at sync/runtime, or {\"env\":\"ENV_VAR_NAME\",\"prefix\":\"Bearer \"} for common authorization formats.",
12
+ "secret_safety": "Resolved secret values must not be written back to this registry file. Missing env vars warn during sync and do not emit empty secret headers.",
13
+ "sync_denylist": "Array of client:server strings skipped by proactive registry sync, for example gemini:tfx-hub."
14
+ },
15
+ "servers": {
16
+ "tfx-hub": {
17
+ "transport": "hub-url",
18
+ "url": "http://127.0.0.1:27888/mcp",
19
+ "safe": true,
20
+ "targets": ["claude", "gemini", "codex"],
21
+ "description": "triflux Hub MCP 서버"
22
+ },
23
+ "context7": {
24
+ "transport": "http",
25
+ "url": "https://mcp.context7.com/mcp",
26
+ "safe": true,
27
+ "targets": ["claude", "gemini", "codex"],
28
+ "description": "Upstash Context7 — 라이브러리 문서/코드 컨텍스트 (HTTP MCP, API key 불필요)"
29
+ }
30
+ },
31
+ "policies": {
32
+ "stdio_action": "replace-with-hub",
33
+ "unknown_server_action": "warn",
34
+ "sync_denylist": [],
35
+ "watched_paths": [
36
+ "~/.gemini/settings.json",
37
+ "~/.codex/config.toml",
38
+ "~/.claude/settings.json",
39
+ "~/.claude/settings.local.json",
40
+ ".claude/mcp.json",
41
+ ".mcp.json"
42
+ ]
43
+ }
44
+ }
@@ -66,9 +66,17 @@ const TIER_PRIORITY = { pro: 0, plus: 1, unknown: 2, free: 3 };
66
66
  const LEASE_TTL_MS = 30 * 60 * 1000; // 30 minutes
67
67
  const CIRCUIT_WINDOW_MS = 10 * 60_000; // 10 minutes
68
68
  const CIRCUIT_MAX_FAILURES = 3;
69
- const AUTH_BASE_PATH = join(homedir(), ".claude", "cache", "tfx-hub");
70
- const CODEX_AUTH_SOURCE_PATH = join(homedir(), ".codex", "auth.json");
71
- const STATE_PERSIST_PATH = join(AUTH_BASE_PATH, "broker-state.json");
69
+ function getAuthBasePath() {
70
+ return join(homedir(), ".claude", "cache", "tfx-hub");
71
+ }
72
+
73
+ function getCodexAuthSourcePath() {
74
+ return join(homedir(), ".codex", "auth.json");
75
+ }
76
+
77
+ function getStatePersistPath() {
78
+ return join(getAuthBasePath(), "broker-state.json");
79
+ }
72
80
  const AUTH_SYNC_LOCK_TIMEOUT_MS = 5_000;
73
81
  const AUTH_SYNC_LOCK_RETRY_MS = 25;
74
82
  const AUTH_SYNC_LOCK_STALE_MS = 30_000;
@@ -78,6 +86,8 @@ const AUTH_SYNC_SAB = new Int32Array(new SharedArrayBuffer(4));
78
86
 
79
87
  function persistState(stateMap) {
80
88
  try {
89
+ const authBasePath = getAuthBasePath();
90
+ const statePersistPath = getStatePersistPath();
81
91
  const now = Date.now();
82
92
  const entries = {};
83
93
  for (const [id, acct] of stateMap) {
@@ -96,8 +106,8 @@ function persistState(stateMap) {
96
106
  };
97
107
  }
98
108
  }
99
- mkdirSync(AUTH_BASE_PATH, { recursive: true });
100
- writeFileSync(STATE_PERSIST_PATH, JSON.stringify({ ts: now, entries }));
109
+ mkdirSync(authBasePath, { recursive: true });
110
+ writeFileSync(statePersistPath, JSON.stringify({ ts: now, entries }));
101
111
  } catch (err) {
102
112
  try {
103
113
  console.error("[account-broker] persistState failed:", err.message);
@@ -107,8 +117,9 @@ function persistState(stateMap) {
107
117
 
108
118
  function loadPersistedState() {
109
119
  try {
110
- if (!existsSync(STATE_PERSIST_PATH)) return null;
111
- return JSON.parse(readFileSync(STATE_PERSIST_PATH, "utf8"));
120
+ const statePersistPath = getStatePersistPath();
121
+ if (!existsSync(statePersistPath)) return null;
122
+ return JSON.parse(readFileSync(statePersistPath, "utf8"));
112
123
  } catch {
113
124
  return null;
114
125
  }
@@ -239,6 +250,19 @@ function withLockFile(lockPath, opts, task) {
239
250
 
240
251
  // ── AccountBroker ────────────────────────────────────────────────
241
252
 
253
+ /**
254
+ * AccountBroker는 codex/gemini 계정 풀 + lease/cooldown/circuit breaker 를 관리한다.
255
+ *
256
+ * **Resolved-at-construction 정책 (lazy/eager mix 주의)**
257
+ *
258
+ * `getAuthBasePath()` / `getCodexAuthSourcePath()` 는 호출 시점에 `homedir()` 를
259
+ * 다시 평가하는 lazy getter 다. 그러나 default 파라미터 (`_authBasePath = getAuthBasePath()`)
260
+ * 는 생성자 호출 시점에 한 번만 평가되고 결과가 `#authBasePath` 등 private field 에 박힌다.
261
+ *
262
+ * 결과: 생성자 이후 process.env.HOME 변경은 broker 가 보는 경로에 전파되지 않는다.
263
+ * 테스트에서 HOME 을 갈아끼우려면 `new AccountBroker(config, { _authBasePath, _codexAuthSourcePath })`
264
+ * 로 오버라이드를 직접 주입하거나, broker 자체를 재생성해야 한다.
265
+ */
242
266
  class AccountBroker extends EventEmitter {
243
267
  #config;
244
268
  #state; // Map<accountId, accountState>
@@ -253,8 +277,8 @@ class AccountBroker extends EventEmitter {
253
277
  config,
254
278
  {
255
279
  _skipPersistence = false,
256
- _authBasePath = AUTH_BASE_PATH,
257
- _codexAuthSourcePath = CODEX_AUTH_SOURCE_PATH,
280
+ _authBasePath = getAuthBasePath(),
281
+ _codexAuthSourcePath = getCodexAuthSourcePath(),
258
282
  _authSyncLockTimeoutMs = AUTH_SYNC_LOCK_TIMEOUT_MS,
259
283
  _authSyncLockRetryMs = AUTH_SYNC_LOCK_RETRY_MS,
260
284
  _authSyncLockStaleMs = AUTH_SYNC_LOCK_STALE_MS,
@@ -2,7 +2,7 @@
2
2
  // Phase 2: codex-adapter.mjs에서 추출한 재사용 가능 유틸리티
3
3
 
4
4
  import { execSync, spawn } from "node:child_process";
5
- import { existsSync, readFileSync } from "node:fs";
5
+ import { existsSync, readFileSync, statSync } from "node:fs";
6
6
 
7
7
  import { IS_WINDOWS, killProcess } from "./platform.mjs";
8
8
 
@@ -391,7 +391,18 @@ export async function runProcess(command, workdir, timeout, opts = {}) {
391
391
  });
392
392
  }
393
393
 
394
+ const resultFileSignature = () => {
395
+ if (!resultFile) return "";
396
+ try {
397
+ const info = statSync(resultFile);
398
+ return `${info.size}:${info.mtimeMs}`;
399
+ } catch {
400
+ return "";
401
+ }
402
+ };
403
+
394
404
  let lastBytes = 0;
405
+ let lastResultFileSignature = resultFileSignature();
395
406
  let lastChange = Date.now();
396
407
  const touch = () => {
397
408
  lastChange = Date.now();
@@ -420,8 +431,14 @@ export async function runProcess(command, workdir, timeout, opts = {}) {
420
431
  }, timeout);
421
432
  const stallTimer = setInterval(() => {
422
433
  const size = Buffer.byteLength(stdout) + Buffer.byteLength(stderr);
423
- if (size !== lastBytes) {
434
+ const currentResultFileSignature = resultFileSignature();
435
+ if (
436
+ size !== lastBytes ||
437
+ currentResultFileSignature !== lastResultFileSignature
438
+ ) {
424
439
  lastBytes = size;
440
+ lastResultFileSignature = currentResultFileSignature;
441
+ touch();
425
442
  return;
426
443
  }
427
444
  if (Date.now() - lastChange >= stallThresholdMs)
@@ -1,25 +1,9 @@
1
- import { psmuxExec } from "./psmux.mjs";
2
- import { detectMultiplexer, hasWindowsTerminal, tmuxExec } from "./session.mjs";
3
- import { createWtManager } from "./wt-manager.mjs";
4
-
5
- function sanitizeWindowTitle(value, fallback = "triflux") {
6
- const text = String(value || "")
7
- .replace(/[\r\n]+/g, " ")
8
- .trim();
9
- return text || fallback;
10
- }
1
+ import { createTerminalOpener } from "./terminal-opener.mjs";
11
2
 
12
3
  function sanitizeSessionName(value) {
13
4
  return String(value || "").replace(/[^a-zA-Z0-9_-]/g, "") || "tfx-session";
14
5
  }
15
6
 
16
- function sanitizeWorkingDirectory(value) {
17
- const text = String(value || "")
18
- .replace(/[\r\n\x00-\x1f]/g, "")
19
- .trim();
20
- return text || process.cwd();
21
- }
22
-
23
7
  export function parseWorkerNumber(value) {
24
8
  const text = String(value || "").trim();
25
9
  const workerMatch = text.match(/^worker-(\d+)$/i);
@@ -29,118 +13,43 @@ export function parseWorkerNumber(value) {
29
13
  return null;
30
14
  }
31
15
 
32
- export function decideDashboardOpenMode({
33
- openAll = false,
34
- hasWtSession = !!process.env.WT_SESSION,
35
- } = {}) {
36
- if (openAll) return hasWtSession ? "tab" : "window";
37
- return hasWtSession ? "split" : "window";
16
+ function ignoreAsyncFailure(value) {
17
+ if (value && typeof value.then === "function") void value.catch(() => {});
38
18
  }
39
19
 
40
- async function spawnWindowsTerminal(spec, opts = {}) {
41
- if (!hasWindowsTerminal()) return false;
42
-
43
- const wt = createWtManager();
44
- const {
45
- mode = "window",
46
- title = "triflux",
47
- cwd = process.cwd(),
48
- split = { orientation: "H", size: 0.5 },
49
- } = opts;
50
-
51
- const safeTitle = sanitizeWindowTitle(title);
52
- const safeCwd = sanitizeWorkingDirectory(cwd);
53
-
54
- try {
55
- if (mode === "split") {
56
- await wt.splitPane({
57
- direction: split?.orientation === "V" ? "V" : "H",
58
- size: (split?.size || 0.5) * 100,
59
- title: safeTitle,
60
- cwd: safeCwd,
61
- command: spec.args
62
- ? `${spec.command} ${spec.args.join(" ")}`
63
- : spec.command,
64
- profile: "triflux",
65
- });
66
- } else {
67
- await wt.createTab({
68
- title: safeTitle,
69
- cwd: safeCwd,
70
- command: spec.args
71
- ? `${spec.command} ${spec.args.join(" ")}`
72
- : spec.command,
73
- profile: "triflux",
74
- });
75
- }
76
- return true;
77
- } catch {
78
- return false;
79
- }
80
- }
20
+ export async function openHeadlessDashboardTarget(sessionName, opts = {}) {
21
+ const { openAll = false, cwd = process.cwd(), title } = opts;
22
+ const safeSession = sanitizeSessionName(sessionName);
23
+ const workerNumber =
24
+ opts.workerNumber ??
25
+ (opts.worker == null ? null : parseWorkerNumber(opts.worker));
81
26
 
82
- async function spawnMacTerminal(spec, opts = {}) {
83
- const mux = detectMultiplexer();
84
- if (mux === "tmux") {
85
- try {
86
- const title = sanitizeWindowTitle(opts.title);
87
- const command = spec.args
88
- ? `${spec.command} ${spec.args.join(" ")}`
89
- : spec.command;
90
- tmuxExec(`new-window -n "${title}" "${command}"`);
91
- return true;
92
- } catch {
93
- return false;
94
- }
95
- }
96
- // tmux 없으면 기본 터미널
27
+ let opener;
97
28
  try {
98
- const { exec } = await import("node:child_process");
99
- exec(`open -a Terminal`, { timeout: 5000 });
100
- return true;
29
+ const deps = opts._deps ?? {};
30
+ const openerFactory = deps.createTerminalOpener ?? createTerminalOpener;
31
+ opener = openerFactory(deps);
101
32
  } catch {
102
- return false;
33
+ return !openAll && workerNumber != null;
103
34
  }
104
- }
105
-
106
- export function openHeadlessDashboardTarget(sessionName, opts = {}) {
107
- const { worker = null, openAll = false, cwd = process.cwd(), title } = opts;
108
-
109
- const safeSession = sanitizeSessionName(sessionName);
110
- const workerNumber = worker == null ? null : parseWorkerNumber(worker);
111
35
 
112
36
  // 선택 워커 → pane focus만 (새 창 열지 않음)
113
37
  if (!openAll && workerNumber != null) {
114
38
  try {
115
- const mux = detectMultiplexer();
116
- if (mux === "psmux") {
117
- psmuxExec(["select-pane", "-t", `${safeSession}:0.${workerNumber}`]);
118
- } else if (
119
- mux === "tmux" ||
120
- mux === "wsl-tmux" ||
121
- mux === "git-bash-tmux"
122
- ) {
123
- tmuxExec(`select-pane -t ${safeSession}:0.${workerNumber}`);
124
- }
39
+ ignoreAsyncFailure(opener.focusPane(safeSession, workerNumber));
125
40
  } catch {}
126
41
  return true;
127
42
  }
128
43
 
129
44
  // 전체 열기 (Shift+Enter) → 새 창으로 세션 attach
130
- if (process.platform === "win32") {
131
- void spawnWindowsTerminal(
132
- { command: "psmux", args: ["attach-session", "-t", safeSession] },
133
- {
134
- mode: decideDashboardOpenMode({ openAll }),
135
- title: title || `▲ ${safeSession}`,
136
- cwd,
137
- },
138
- );
139
- } else {
140
- void spawnMacTerminal(
141
- { command: "tmux", args: ["attach-session", "-t", safeSession] },
142
- { title: title || `▲ ${safeSession}`, cwd },
143
- );
45
+ try {
46
+ const opened = opener.openSession(safeSession, {
47
+ title: title || `▲ ${safeSession}`,
48
+ cwd,
49
+ profile: opts.profile ?? "triflux",
50
+ });
51
+ return await opened;
52
+ } catch {
53
+ return false;
144
54
  }
145
- return true;
146
55
  }
@@ -1370,6 +1370,7 @@ export async function autoAttachTerminal(
1370
1370
  opts = {},
1371
1371
  workerCount = 2,
1372
1372
  ) {
1373
+ if (process.platform !== "win32") return false;
1373
1374
  if (!process.env.WT_SESSION) return false;
1374
1375
 
1375
1376
  const wt = createWtManager();
@@ -18,6 +18,13 @@ function escapePowerShellSingleQuoted(value) {
18
18
  return String(value ?? "").replaceAll("'", "''");
19
19
  }
20
20
 
21
+ function escapeAppleScriptString(value) {
22
+ return String(value ?? "")
23
+ .replaceAll("\\", "\\\\")
24
+ .replaceAll('"', '\\"')
25
+ .replace(/\r?\n/g, " ");
26
+ }
27
+
21
28
  function normalizeTimestamp(value) {
22
29
  if (value instanceof Date) return value.toISOString();
23
30
  if (value == null || value === "") return new Date().toISOString();
@@ -215,8 +222,8 @@ async function sendToast(event, config, deps) {
215
222
  if ((deps.platform || process.platform) === "darwin") {
216
223
  const title = formatEventTitle(event);
217
224
  const body = formatEventBody(event);
218
- const safeTitle = title.replace(/\\/g, "\\\\").replace(/'/g, "'\"'\"'");
219
- const safeBody = body.replace(/\\/g, "\\\\").replace(/'/g, "'\"'\"'");
225
+ const safeTitle = escapeAppleScriptString(title);
226
+ const safeBody = escapeAppleScriptString(body);
220
227
  try {
221
228
  await execFileAsync(
222
229
  "osascript",
@@ -60,9 +60,25 @@ export function createPsmuxRuntime(adapter = defaultPsmuxAdapter) {
60
60
  // tmux 어댑터
61
61
  // ---------------------------------------------------------------------------
62
62
 
63
+ // tmux 세션 이름은 tmuxExec 명령 문자열에 직접 보간되므로 shell 메타문자가 들어오면 위험.
64
+ // 현 호출처는 모두 safe identifier (slug/uuid 류) 이지만 어댑터 export 가 contract 를
65
+ // 강제하지 않아 외부 호출자가 임의 문자열을 주입할 수 있다 → 보수적으로 검증.
66
+ const TMUX_SAFE_SESSION_NAME = /^[A-Za-z0-9_.:-]+$/;
67
+
68
+ function assertSafeTmuxSessionName(sessionName) {
69
+ const value = String(sessionName ?? "");
70
+ if (!TMUX_SAFE_SESSION_NAME.test(value)) {
71
+ throw new Error(
72
+ `Unsafe tmux session name: ${JSON.stringify(value)} (allowed: A-Za-z0-9_.:-)`,
73
+ );
74
+ }
75
+ return value;
76
+ }
77
+
63
78
  function tmuxSessionExists(sessionName) {
79
+ const safe = assertSafeTmuxSessionName(sessionName);
64
80
  try {
65
- tmuxExec(`has-session -t ${sessionName}`);
81
+ tmuxExec(`has-session -t ${safe}`);
66
82
  return true;
67
83
  } catch {
68
84
  return false;
@@ -70,12 +86,25 @@ function tmuxSessionExists(sessionName) {
70
86
  }
71
87
 
72
88
  function createTmuxSession(sessionName, opts = {}) {
73
- tmuxExec(`new-session -d -s ${sessionName} -x 220 -y 55`);
89
+ const safe = assertSafeTmuxSessionName(sessionName);
90
+ tmuxExec(`new-session -d -s ${safe} -x 220 -y 55`);
91
+ }
92
+
93
+ function sendPromptToTmuxSession(sessionName, prompt) {
94
+ const safe = assertSafeTmuxSessionName(sessionName);
95
+ const quotedPrompt = String(prompt).replace(/'/g, "'\\''");
96
+ tmuxExec(`send-keys -t ${safe}:0.0 '${quotedPrompt}' Enter`);
74
97
  }
75
98
 
76
99
  function killTmuxSessionByName(sessionName) {
100
+ let safe;
77
101
  try {
78
- tmuxExec(`kill-session -t ${sessionName}`);
102
+ safe = assertSafeTmuxSessionName(sessionName);
103
+ } catch {
104
+ return;
105
+ }
106
+ try {
107
+ tmuxExec(`kill-session -t ${safe}`);
79
108
  } catch {
80
109
  // 이미 종료된 세션 — 무시
81
110
  }
@@ -83,6 +112,7 @@ function killTmuxSessionByName(sessionName) {
83
112
 
84
113
  const defaultTmuxAdapter = {
85
114
  createSession: createTmuxSession,
115
+ sendPrompt: sendPromptToTmuxSession,
86
116
  killSession: killTmuxSessionByName,
87
117
  hasSession: tmuxSessionExists,
88
118
  };
@@ -90,28 +120,55 @@ const defaultTmuxAdapter = {
90
120
  /**
91
121
  * @param {{
92
122
  * createSession: typeof createTmuxSession,
123
+ * sendPrompt: typeof sendPromptToTmuxSession,
93
124
  * killSession: typeof killTmuxSessionByName,
94
125
  * hasSession: typeof tmuxSessionExists,
95
- * }} [adapter]
96
- * @returns {TeamRuntime & { name: "tmux" }}
126
+ * }} [opts.adapter]
127
+ * @param {string} [opts.sessionName]
128
+ * @returns {{
129
+ * name: "tmux",
130
+ * kind: "tmux",
131
+ * sessionName: string,
132
+ * createSession: (opts?: object) => unknown,
133
+ * sendPrompt: (prompt: string) => unknown,
134
+ * start: (sessionName: string, opts?: object) => unknown,
135
+ * stop: (sessionName: string) => void,
136
+ * isAlive: (sessionName: string) => boolean,
137
+ * getStatus: (sessionName: string) => RuntimeStatus,
138
+ * }}
97
139
  */
98
- export function createTmuxRuntime(adapter = defaultTmuxAdapter) {
140
+ export function createTmuxRuntime(opts = {}) {
141
+ const { adapter = defaultTmuxAdapter, sessionName = "" } = opts;
142
+
99
143
  return {
100
144
  name: "tmux",
101
- start(sessionName, opts = {}) {
102
- return adapter.createSession(sessionName, opts);
145
+ kind: "tmux",
146
+ sessionName,
147
+ // start() 와 동일하게 첫 인자로 sessionName override 를 받는다.
148
+ // 첫 인자가 string 이 아니면 (옵션 객체 등) closure sessionName 을 사용하는 legacy 형태로 폴백.
149
+ createSession(sessionNameArg, opts) {
150
+ if (typeof sessionNameArg === "string") {
151
+ return adapter.createSession(sessionNameArg, opts ?? {});
152
+ }
153
+ return adapter.createSession(sessionName, sessionNameArg ?? {});
103
154
  },
104
- stop(sessionName) {
105
- adapter.killSession(sessionName);
155
+ sendPrompt(prompt, sessionNameArg = sessionName) {
156
+ return adapter.sendPrompt(sessionNameArg, prompt);
106
157
  },
107
- isAlive(sessionName) {
108
- return adapter.hasSession(sessionName);
158
+ start(sessionNameArg = sessionName, opts = {}) {
159
+ return adapter.createSession(sessionNameArg, opts);
109
160
  },
110
- getStatus(sessionName) {
161
+ stop(sessionNameArg = sessionName) {
162
+ adapter.killSession(sessionNameArg);
163
+ },
164
+ isAlive(sessionNameArg = sessionName) {
165
+ return adapter.hasSession(sessionNameArg);
166
+ },
167
+ getStatus(sessionNameArg = sessionName) {
111
168
  return {
112
169
  name: "tmux",
113
- sessionName,
114
- alive: adapter.hasSession(sessionName),
170
+ sessionName: sessionNameArg,
171
+ alive: adapter.hasSession(sessionNameArg),
115
172
  };
116
173
  },
117
174
  };
@@ -119,9 +176,10 @@ export function createTmuxRuntime(adapter = defaultTmuxAdapter) {
119
176
 
120
177
  /**
121
178
  * @param {string} mode
179
+ * @param {object} [opts]
122
180
  * @returns {TeamRuntime & { name: string }}
123
181
  */
124
- export function createRuntime(mode) {
182
+ export function createRuntime(mode, opts = {}) {
125
183
  const normalizedMode = String(mode || "")
126
184
  .trim()
127
185
  .toLowerCase();
@@ -131,7 +189,7 @@ export function createRuntime(mode) {
131
189
  }
132
190
 
133
191
  if (normalizedMode === "tmux") {
134
- return createTmuxRuntime();
192
+ return createTmuxRuntime(opts);
135
193
  }
136
194
 
137
195
  if (normalizedMode === "native" || normalizedMode === "wt") {