import { readFileSync, readdirSync, readlinkSync } from 'fs'; import { execFileSync } from 'child_process'; function read_file(path) { try { return readFileSync(path, 'utf8'); } catch { return null; } } function get_exe(pid) { try { return readlinkSync(`/proc/${pid}/exe`); } catch { return null; } } function get_cmdline(pid) { const raw = read_file(`/proc/${pid}/cmdline`); if (!raw) { return null; } return raw.replace(/\0/g, ' ').trim(); } function get_ppid(pid) { const status = read_file(`/proc/${pid}/status`); if (!status) { return null; } const match = status.match(/^PPid:\s+(\d+)/m); return match ? parseInt(match[1]) : null; } //const DOCKER_COMM_PATTERN = /^docker-compose$/; const CLAUDE_CMDLINE_SIGNATURE = 'claude --dangerously-skip-permissions'; const DOCKER_COMM_PATTERN = /^docker-compose$/; function get_comm(pid) { return read_file(`/proc/${pid}/comm`)?.trim() ?? null; } export function list_docker_pids() { const results = []; for (const entry of readdirSync('/proc')) { if (!/^\d+$/.test(entry)) { continue; } const comm = get_comm(entry); if (!comm || !DOCKER_COMM_PATTERN.test(comm)) { continue; } const cmdline = get_cmdline(entry); if (cmdline && cmdline.includes(CLAUDE_CMDLINE_SIGNATURE)) { results.push({ pid: entry, comm, cmdline }); } } return results; } export function walk_to_konsole(pid) { let current = String(pid); const visited = new Set(); const chain = []; while (current && current !== '0' && !visited.has(current)) { visited.add(current); const exe = get_exe(current); chain.push({ pid: current, exe }); if (exe === '/usr/bin/konsole') { return { konsole_pid: current, chain }; } current = String(get_ppid(current)); } return { konsole_pid: null, chain }; } function get_net_client_list() { try { const out = execFileSync('xprop', ['-root', '_NET_CLIENT_LIST'], { encoding: 'utf8' }); // Format: _NET_CLIENT_LIST(WINDOW): window id # 0x..., 0x..., ... const match = out.match(/window id # (.+)/); if (!match) { return []; } return match[1].split(',').map(s => parseInt(s.trim(), 16)).filter(Boolean); } catch { return []; } } function get_window_pid(window_id) { try { const hex = '0x' + window_id.toString(16); const out = execFileSync('xprop', ['-id', hex, '_NET_WM_PID'], { encoding: 'utf8' }); const match = out.match(/= (\d+)/); return match ? parseInt(match[1]) : null; } catch { return null; } } export function window_id_for_pid(pid) { const target_pid = parseInt(pid); for (const wid of get_net_client_list()) { if (get_window_pid(wid) === target_pid) { return '0x' + wid.toString(16); } } return null; } export function find_claude_window_by_process_tree({ debug = false } = {}) { const docker_pids = list_docker_pids(); if (debug) { console.error(`[find-window] docker pids (${docker_pids.length}):`); for (const { pid, comm, cmdline } of docker_pids) { console.error(` pid=${pid} comm=${comm} cmdline=${cmdline}`); } } for (const { pid } of docker_pids) { const { konsole_pid, chain } = walk_to_konsole(pid); if (debug) { const chain_str = chain.map(e => `${e.pid}(${e.exe?.split('/').pop() ?? '?'})`).join(' → '); console.error(`[find-window] pid=${pid} chain: ${chain_str}`); } if (!konsole_pid) { continue; } if (debug) { console.error(`[find-window] found konsole pid=${konsole_pid}, looking up window...`); } const window_id = window_id_for_pid(konsole_pid); if (debug) { console.error(`[find-window] window id: ${window_id ?? 'not found'}`); } if (window_id) { return window_id; } } return null; } export function find_claude_window_by_title(title = 'claude-docker') { try { const result = execFileSync( 'xdotool', ['search', '--name', title], { encoding: 'utf8' } ); const ids = result.trim().split('\n').filter(Boolean); return ids[0] ?? null; } catch { return null; } } export function find_claude_window({ debug = false } = {}) { return find_claude_window_by_process_tree({ debug }) ?? find_claude_window_by_title(); }