Swadhin Biswas feat: implement mervelas cli engine, built-in tools, and reactive UI · 4/3/2026
0358986
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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
87
88
89
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
119
120
121
122
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
/**
* TMUX SOCKET ISOLATION
* =====================
* This module manages an isolated tmux socket for Claude's operations.
*
* WHY THIS EXISTS:
* Without isolation, Claude could accidentally affect the user's tmux sessions.
* For example, running `tmux kill-session` via the Bash tool would kill the
* user's current session if they started Claude from within tmux.
*
* HOW IT WORKS:
* 1. Claude creates its own tmux socket: `claude-<PID>` (e.g., `claude-12345`)
* 2. ALL Tmux tool commands use this socket via the `-L` flag
* 3. ALL Bash tool commands inherit TMUX env var pointing to this socket
* (set in Shell.ts via getClaudeTmuxEnv())
*
* This means ANY tmux command run through Claude - whether via the Tmux tool
* directly or via Bash - will operate on Claude's isolated socket, NOT the
* user's tmux session.
*
* IMPORTANT: The user's original TMUX env var is NOT used. After socket
* initialization, getClaudeTmuxEnv() returns a value that overrides the
* user's TMUX in all child processes spawned by Shell.ts.
*/
import { posix } from 'path'
import { registerCleanup } from './cleanupRegistry.js'
import { logForDebugging } from './debug.js'
import { toError } from './errors.js'
import { execFileNoThrow } from './execFileNoThrow.js'
import { logError } from './log.js'
import { getPlatform } from './platform.js'
// Constants for tmux socket management
const TMUX_COMMAND = 'tmux'
const CLAUDE_SOCKET_PREFIX = 'claude'
/**
* Executes a tmux command, routing through WSL on Windows.
* On Windows, tmux only exists inside WSL — WSL interop lets the tmux session
* launch .exe files as native Win32 processes while stdin/stdout flow through
* the WSL pty.
*/
async function execTmux(
args: string[],
opts?: { useCwd?: boolean },
): Promise<{ stdout: string; stderr: string; code: number }> {
if (getPlatform() === 'windows') {
// -e execs tmux directly without the login shell. Without it, wsl hands the
// command line to bash which eats `#` as a comment: `display-message -p
// #{socket_path},#{pid}` below becomes `display-message -p ` → exit 1 →
// we silently fall back to the guessed path and never learn the real
// server PID. Same root cause as TungstenTool/utils.ts:execTmuxCommand.
const result = await execFileNoThrow('wsl', ['-e', TMUX_COMMAND, ...args], {
env: { ...process.env, WSL_UTF8: '1' },
...opts,
})
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
code: result.code || 0,
}
}
const result = await execFileNoThrow(TMUX_COMMAND, args, opts)
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
code: result.code || 0,
}
}
// Socket state - initialized lazily when Tmux tool is first used or a tmux command is run
let socketName: string | null = null
let socketPath: string | null = null
let serverPid: number | null = null
let isInitializing = false
let initPromise: Promise<void> | null = null
// tmux availability - checked once upfront
let tmuxAvailabilityChecked = false
let tmuxAvailable = false
// Track whether the Tmux tool has been used at least once
// Used to defer socket initialization until actually needed
let tmuxToolUsed = false
/**
* Gets the socket name for Claude's isolated tmux session.
* Format: claude-<PID>
*/
export function getClaudeSocketName(): string {
if (!socketName) {
socketName = `${CLAUDE_SOCKET_PREFIX}-${process.pid}`
}
return socketName
}
/**
* Gets the socket path if the socket has been initialized.
* Returns null if not yet initialized.
*/
export function getClaudeSocketPath(): string | null {
return socketPath
}
/**
* Sets socket info after initialization.
* Called after the tmux session is created.
*/
export function setClaudeSocketInfo(path: string, pid: number): void {
socketPath = path
serverPid = pid
}
/**
* Returns whether the socket has been initialized.
*/
export function isSocketInitialized(): boolean {
return socketPath !== null && serverPid !== null
}
/**
* Gets the TMUX environment variable value for Claude's isolated socket.
*
* CRITICAL: This value is used by Shell.ts to override the TMUX env var
* in ALL child processes. This ensures that any `tmux` command run via
* the Bash tool will operate on Claude's socket, NOT the user's session.
*
* Format: "socket_path,server_pid,pane_index" (matches tmux's TMUX env var)
* Example: "/tmp/tmux-501/claude-12345,54321,0"
*
* Returns null if socket is not yet initialized.
* When null, Shell.ts does not override TMUX, preserving user's environment.
*/
export function getClaudeTmuxEnv(): string | null {
if (!socketPath || serverPid === null) {
return null
}
return `${socketPath},${serverPid},0`
}
/**
* Checks if tmux is available on this system.
* This is checked once and cached for the lifetime of the process.
*
* When tmux is not available:
* - TungstenTool (Tmux) will not work
* - TeammateTool will not work (it uses tmux for pane management)
* - Bash commands will run without tmux isolation
*/
export async function checkTmuxAvailable(): Promise<boolean> {
if (!tmuxAvailabilityChecked) {
const result =
getPlatform() === 'windows'
? await execFileNoThrow('wsl', ['-e', TMUX_COMMAND, '-V'], {
env: { ...process.env, WSL_UTF8: '1' },
useCwd: false,
})
: await execFileNoThrow('which', [TMUX_COMMAND], {
useCwd: false,
})
tmuxAvailable = result.code === 0
if (!tmuxAvailable) {
logForDebugging(
`[Socket] tmux is not installed. The Tmux tool and Teammate tool will not be available.`,
)
}
tmuxAvailabilityChecked = true
}
return tmuxAvailable
}
/**
* Returns the cached tmux availability status.
* Returns false if availability hasn't been checked yet.
* Use checkTmuxAvailable() to perform the check.
*/
export function isTmuxAvailable(): boolean {
return tmuxAvailabilityChecked && tmuxAvailable
}
/**
* Marks that the Tmux tool has been used at least once.
* Called by TungstenTool before initialization.
* After this is called, Shell.ts will initialize the socket for subsequent Bash commands.
*/
export function markTmuxToolUsed(): void {
tmuxToolUsed = true
}
/**
* Returns whether the Tmux tool has been used at least once.
* Used by Shell.ts to decide whether to initialize the socket.
*/
export function hasTmuxToolBeenUsed(): boolean {
return tmuxToolUsed
}
/**
* Ensures the socket is initialized with a tmux session.
* Called by Shell.ts when the Tmux tool has been used or the command includes "tmux".
* Safe to call multiple times; will only initialize once.
*
* If tmux is not installed, this function returns gracefully without
* initializing the socket. getClaudeTmuxEnv() will return null, and
* Bash commands will run without tmux isolation.
*/
export async function ensureSocketInitialized(): Promise<void> {
// Already initialized
if (isSocketInitialized()) {
return
}
// Check if tmux is available before trying to use it
const available = await checkTmuxAvailable()
if (!available) {
return
}
// Another call is already initializing - wait for it but don't propagate errors
// The original caller handles the error and sets up graceful degradation
if (isInitializing && initPromise) {
try {
await initPromise
} catch {
// Ignore - the original caller logs the error
}
return
}
isInitializing = true
initPromise = doInitialize()
try {
await initPromise
} catch (error) {
// Log error but don't throw - graceful degradation
const err = toError(error)
logError(err)
logForDebugging(
`[Socket] Failed to initialize tmux socket: ${err.message}. Tmux isolation will be disabled.`,
)
} finally {
isInitializing = false
}
}
/**
* Kills the tmux server for Claude's isolated socket.
* Called during graceful shutdown to clean up resources.
*/
async function killTmuxServer(): Promise<void> {
const socket = getClaudeSocketName()
logForDebugging(`[Socket] Killing tmux server for socket: ${socket}`)
const result = await execTmux(['-L', socket, 'kill-server'])
if (result.code === 0) {
logForDebugging(`[Socket] Successfully killed tmux server`)
} else {
// Server may already be dead, which is fine
logForDebugging(
`[Socket] Failed to kill tmux server (exit ${result.code}): ${result.stderr}`,
)
}
}
async function doInitialize(): Promise<void> {
const socket = getClaudeSocketName()
// Create a new session with our custom socket
// Pass CLAUDE_CODE_SKIP_PROMPT_HISTORY via -e so it's set in the initial shell environment
//
// On Windows, the tmux server inherits WSL_INTEROP from the short-lived
// wsl.exe that spawns it; once `new-session -d` detaches and wsl.exe exits,
// that socket stops servicing requests. Any cli.exe launched inside the pane
// then hits `UtilAcceptVsock: accept4 failed 110` (ETIMEDOUT). Observed on
// 2026-03-25: server PID 386 (started alongside /init at WSL boot) inherited
// /run/WSL/383_interop — init's own socket, which listens but doesn't handle
// interop. /run/WSL/1_interop is a stable symlink WSL maintains to the real
// handler; pin the server to it so interop survives the spawning wsl.exe.
const result = await execTmux([
'-L',
socket,
'new-session',
'-d',
'-s',
'base',
'-e',
'CLAUDE_CODE_SKIP_PROMPT_HISTORY=true',
...(getPlatform() === 'windows'
? ['-e', 'WSL_INTEROP=/run/WSL/1_interop']
: []),
])
if (result.code !== 0) {
// Session might already exist from a previous run with same PID (unlikely but possible)
// Check if the session exists
const checkResult = await execTmux([
'-L',
socket,
'has-session',
'-t',
'base',
])
if (checkResult.code !== 0) {
throw new Error(
`Failed to create tmux session on socket ${socket}: ${result.stderr}`,
)
}
}
// Register cleanup to kill the tmux server on exit
registerCleanup(killTmuxServer)
// Set CLAUDE_CODE_SKIP_PROMPT_HISTORY in the tmux GLOBAL environment (-g).
// Without -g this would only apply to the 'base' session, and new sessions
// created by TungstenTool (e.g. 'test', 'verify') would not inherit it.
// Any Mervelas instance spawned on this socket will inherit this env var,
// preventing test/verification sessions from polluting the user's real
// command history and --resume session list.
await execTmux([
'-L',
socket,
'set-environment',
'-g',
'CLAUDE_CODE_SKIP_PROMPT_HISTORY',
'true',
])
// Same WSL_INTEROP pin as the new-session -e above, but in the GLOBAL env
// so sessions created by TungstenTool inherit it too. The -e on new-session
// only covers the base session's initial shell; a later `new-session -s cc`
// inherits the SERVER's env, which still holds the stale socket from the
// wsl.exe that spawned it.
if (getPlatform() === 'windows') {
await execTmux([
'-L',
socket,
'set-environment',
'-g',
'WSL_INTEROP',
'/run/WSL/1_interop',
])
}
// Get the socket path and server PID
const infoResult = await execTmux([
'-L',
socket,
'display-message',
'-p',
'#{socket_path},#{pid}',
])
if (infoResult.code === 0) {
const [path, pidStr] = infoResult.stdout.trim().split(',')
if (path && pidStr) {
const pid = parseInt(pidStr, 10)
if (!isNaN(pid)) {
setClaudeSocketInfo(path, pid)
return
}
}
// Parsing failed - log and fall through to fallback
logForDebugging(
`[Socket] Failed to parse socket info from tmux output: "${infoResult.stdout.trim()}". Using fallback path.`,
)
} else {
// Command failed - log and fall through to fallback
logForDebugging(
`[Socket] Failed to get socket info via display-message (exit ${infoResult.code}): ${infoResult.stderr}. Using fallback path.`,
)
}
// Fallback: construct the socket path from standard tmux location
// tmux sockets are typically at $TMPDIR/tmux-<UID>/<socket_name> (or /tmp/tmux-<UID>/ if TMPDIR is not set)
// On Windows this path is inside WSL, so always use POSIX separators.
// process.getuid() is undefined on Windows; WSL default user is root (uid 0) in CI.
const uid = process.getuid?.() ?? 0
const baseTmpDir = process.env.TMPDIR || '/tmp'
const fallbackPath = posix.join(baseTmpDir, `tmux-${uid}`, socket)
// Get server PID separately
const pidResult = await execTmux([
'-L',
socket,
'display-message',
'-p',
'#{pid}',
])
if (pidResult.code === 0) {
const pid = parseInt(pidResult.stdout.trim(), 10)
if (!isNaN(pid)) {
logForDebugging(
`[Socket] Using fallback socket path: ${fallbackPath} (server PID: ${pid})`,
)
setClaudeSocketInfo(fallbackPath, pid)
return
}
// PID parsing failed
logForDebugging(
`[Socket] Failed to parse server PID from tmux output: "${pidResult.stdout.trim()}"`,
)
} else {
logForDebugging(
`[Socket] Failed to get server PID (exit ${pidResult.code}): ${pidResult.stderr}`,
)
}
throw new Error(
`Failed to get socket info for ${socket}: primary="${infoResult.stderr}", fallback="${pidResult.stderr}"`,
)
}
// For testing purposes
export function resetSocketState(): void {
socketName = null
socketPath = null
serverPid = null
isInitializing = false
initPromise = null
tmuxAvailabilityChecked = false
tmuxAvailable = false
tmuxToolUsed = false
}