@epiphytic/claudecodeui 1.2.3 → 1.3.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.
@@ -6,334 +6,35 @@
6
6
  * running simultaneously on the same project.
7
7
  *
8
8
  * Detection methods:
9
- * 1. Process detection via pgrep/ps
10
- * 2. tmux session scanning
9
+ * 1. Process detection via cached process scan (updated every minute)
10
+ * 2. tmux session scanning (from cache)
11
11
  * 3. Session lock file detection (.claude/session.lock)
12
+ *
13
+ * Performance:
14
+ * - Uses cached process data from process-cache.js for instant responses
15
+ * - Only lock file checks are done on-demand (fast filesystem operation)
12
16
  */
13
17
 
14
- import { spawnSync } from "child_process";
15
18
  import fs from "fs";
16
19
  import path from "path";
17
- import os from "os";
18
-
19
- // Cache to avoid repeated process scans
20
- const detectionCache = new Map();
21
- const CACHE_TTL = 5000; // 5 seconds
22
-
23
- /**
24
- * Get the current process ID (for exclusion)
25
- */
26
- const currentPid = process.pid;
27
-
28
- /**
29
- * Detect external Claude processes
30
- * @returns {{ processes: Array<{ pid: number, command: string, cwd: string | null }>, detectionAvailable: boolean, error: string | null }}
31
- */
32
- function detectClaudeProcesses() {
33
- const processes = [];
34
- let detectionAvailable = true;
35
- let error = null;
36
-
37
- console.log("[ExternalSessionDetector] detectClaudeProcesses() called");
38
- console.log("[ExternalSessionDetector] Platform:", os.platform());
39
- console.log("[ExternalSessionDetector] Current PID:", currentPid);
40
-
41
- if (os.platform() === "win32") {
42
- // Windows: use wmic or tasklist
43
- try {
44
- const result = spawnSync(
45
- "wmic",
46
- [
47
- "process",
48
- "where",
49
- "name like '%claude%'",
50
- "get",
51
- "processid,commandline",
52
- ],
53
- { encoding: "utf8", stdio: "pipe" },
54
- );
55
-
56
- if (result.status === 0) {
57
- const lines = result.stdout.trim().split("\n").slice(1); // Skip header
58
- for (const line of lines) {
59
- const match = line.match(/(\d+)\s*$/);
60
- if (match) {
61
- const pid = parseInt(match[1], 10);
62
- if (pid !== currentPid) {
63
- processes.push({ pid, command: line.trim(), cwd: null });
64
- }
65
- }
66
- }
67
- } else if (result.error) {
68
- detectionAvailable = false;
69
- error = `wmic not available: ${result.error.message}`;
70
- }
71
- } catch (e) {
72
- detectionAvailable = false;
73
- error = `Windows process detection failed: ${e.message}`;
74
- }
75
- } else {
76
- // Unix: use pgrep and ps
77
- try {
78
- // First, check if pgrep is available
79
- const pgrepCheck = spawnSync("which", ["pgrep"], {
80
- encoding: "utf8",
81
- stdio: "pipe",
82
- });
83
- console.log(
84
- "[ExternalSessionDetector] pgrep available:",
85
- pgrepCheck.status === 0,
86
- );
87
-
88
- if (pgrepCheck.status !== 0) {
89
- // pgrep not available, try ps aux as fallback
90
- console.log("[ExternalSessionDetector] Using ps aux fallback");
91
- try {
92
- const psResult = spawnSync("ps", ["aux"], {
93
- encoding: "utf8",
94
- stdio: "pipe",
95
- });
96
-
97
- if (psResult.status === 0) {
98
- const lines = psResult.stdout.split("\n");
99
- const claudeLines = lines.filter(
100
- (line) =>
101
- line.toLowerCase().includes("claude") &&
102
- !line.includes(String(currentPid)),
103
- );
104
- console.log(
105
- "[ExternalSessionDetector] ps aux found",
106
- claudeLines.length,
107
- 'lines containing "claude"',
108
- );
109
-
110
- for (const line of claudeLines) {
111
- const parts = line.trim().split(/\s+/);
112
- if (parts.length >= 2) {
113
- const pid = parseInt(parts[1], 10);
114
- if (!isNaN(pid) && pid !== currentPid) {
115
- const command = parts.slice(10).join(" ");
116
- const isExternal = isExternalClaudeProcess(command);
117
- console.log(
118
- `[ExternalSessionDetector] PID ${pid}: "${command.slice(0, 60)}..." isExternal=${isExternal}`,
119
- );
120
- if (isExternal) {
121
- processes.push({ pid, command, cwd: null });
122
- }
123
- }
124
- }
125
- }
126
- } else {
127
- detectionAvailable = false;
128
- error = "Neither pgrep nor ps aux available";
129
- console.log("[ExternalSessionDetector] ps aux failed");
130
- }
131
- } catch (e) {
132
- detectionAvailable = false;
133
- error = `Process detection failed: ${e.message}`;
134
- console.log("[ExternalSessionDetector] ps aux exception:", e.message);
135
- }
136
- } else {
137
- // pgrep is available, use it
138
- const pgrepResult = spawnSync("pgrep", ["-f", "claude"], {
139
- encoding: "utf8",
140
- stdio: "pipe",
141
- });
142
- console.log(
143
- "[ExternalSessionDetector] pgrep status:",
144
- pgrepResult.status,
145
- );
146
-
147
- if (pgrepResult.status === 0) {
148
- const pids = pgrepResult.stdout.trim().split("\n").filter(Boolean);
149
- console.log("[ExternalSessionDetector] pgrep found PIDs:", pids);
150
-
151
- for (const pidStr of pids) {
152
- const pid = parseInt(pidStr, 10);
153
-
154
- // Skip our own process and child processes
155
- if (pid === currentPid) {
156
- console.log(
157
- `[ExternalSessionDetector] Skipping our own PID ${pid}`,
158
- );
159
- continue;
160
- }
161
-
162
- // Get command details
163
- const psResult = spawnSync(
164
- "ps",
165
- ["-p", String(pid), "-o", "args="],
166
- {
167
- encoding: "utf8",
168
- stdio: "pipe",
169
- },
170
- );
171
-
172
- if (psResult.status === 0) {
173
- const command = psResult.stdout.trim();
174
- const isExternal = isExternalClaudeProcess(command);
175
- console.log(
176
- `[ExternalSessionDetector] PID ${pid}: "${command.slice(0, 80)}..." isExternal=${isExternal}`,
177
- );
178
-
179
- // Filter out our own subprocesses (claude-sdk spawned by this app)
180
- // and only include standalone claude CLI invocations
181
- if (isExternal) {
182
- // Try to get working directory via lsof
183
- let cwd = null;
184
- try {
185
- const lsofResult = spawnSync(
186
- "lsof",
187
- ["-p", String(pid), "-Fn"],
188
- {
189
- encoding: "utf8",
190
- stdio: "pipe",
191
- },
192
- );
193
- if (lsofResult.status === 0) {
194
- const cwdMatch = lsofResult.stdout.match(/n(\/[^\n]+)/);
195
- if (cwdMatch) {
196
- cwd = cwdMatch[1];
197
- }
198
- }
199
- } catch {
200
- // lsof may not be available - not critical
201
- }
202
-
203
- processes.push({ pid, command, cwd });
204
- }
205
- }
206
- }
207
- } else {
208
- console.log(
209
- "[ExternalSessionDetector] pgrep found no claude processes (status:",
210
- pgrepResult.status,
211
- ")",
212
- );
213
- }
214
- }
215
- } catch (e) {
216
- detectionAvailable = false;
217
- error = `Unix process detection failed: ${e.message}`;
218
- console.log(
219
- "[ExternalSessionDetector] Unix detection exception:",
220
- e.message,
221
- );
222
- }
223
- }
224
-
225
- return { processes, detectionAvailable, error };
226
- }
227
-
228
- /**
229
- * Check if a command is an external Claude process (not spawned by us)
230
- * @param {string} command - The process command line
231
- * @returns {boolean}
232
- */
233
- function isExternalClaudeProcess(command) {
234
- // Skip node processes (SDK internals)
235
- if (command.startsWith("node ")) {
236
- console.log("[isExternalClaudeProcess] Rejected: starts with 'node '");
237
- return false;
238
- }
239
-
240
- // Skip our own server
241
- if (command.includes("claudecodeui/server")) {
242
- console.log(
243
- "[isExternalClaudeProcess] Rejected: contains 'claudecodeui/server'",
244
- );
245
- return false;
246
- }
247
-
248
- // Look for actual claude CLI invocations
249
- const isExternal =
250
- command.includes("claude ") ||
251
- command.includes("claude-code") ||
252
- command.match(/\/claude\s/) ||
253
- command.endsWith("/claude");
254
-
255
- console.log(
256
- `[isExternalClaudeProcess] "${command.slice(0, 60)}..." => ${isExternal}`,
257
- );
258
- return isExternal;
259
- }
260
-
261
- /**
262
- * Detect tmux sessions that might be running Claude
263
- * @returns {Array<{ sessionName: string, windows: number, attached: boolean }>}
264
- */
265
- function detectClaudeTmuxSessions() {
266
- const sessions = [];
20
+ import { createLogger } from "./logger.js";
21
+ import { getCachedProcessData, getCacheAge } from "./process-cache.js";
267
22
 
268
- try {
269
- const result = spawnSync(
270
- "tmux",
271
- [
272
- "list-sessions",
273
- "-F",
274
- "#{session_name}:#{session_windows}:#{session_attached}",
275
- ],
276
- { encoding: "utf8", stdio: "pipe" },
277
- );
278
-
279
- if (result.status === 0) {
280
- const lines = result.stdout.trim().split("\n").filter(Boolean);
281
-
282
- for (const line of lines) {
283
- const [sessionName, windows, attached] = line.split(":");
284
-
285
- // Skip our own sessions
286
- if (sessionName.startsWith("claudeui-")) continue;
287
-
288
- // Check if session might be running Claude
289
- // We can peek at the pane content or just check window titles
290
- if (mightBeClaudeSession(sessionName)) {
291
- sessions.push({
292
- sessionName,
293
- windows: parseInt(windows, 10),
294
- attached: attached === "1",
295
- });
296
- }
297
- }
298
- }
299
- } catch {
300
- // tmux not available or no server running
301
- }
302
-
303
- return sessions;
304
- }
23
+ const log = createLogger("external-session-detector");
305
24
 
306
25
  /**
307
- * Heuristic check if a tmux session might be running Claude
308
- * @param {string} sessionName - The session name
26
+ * Check if a process exists
27
+ * @param {number} pid - Process ID
309
28
  * @returns {boolean}
310
29
  */
311
- function mightBeClaudeSession(sessionName) {
312
- // Check common patterns
313
- const claudePatterns = ["claude", "ai", "chat", "code"];
314
- const lowerName = sessionName.toLowerCase();
315
-
316
- for (const pattern of claudePatterns) {
317
- if (lowerName.includes(pattern)) return true;
318
- }
319
-
320
- // Try to peek at the session's current command
30
+ function processExists(pid) {
321
31
  try {
322
- const result = spawnSync(
323
- "tmux",
324
- ["display-message", "-t", sessionName, "-p", "#{pane_current_command}"],
325
- { encoding: "utf8", stdio: "pipe" },
326
- );
327
-
328
- if (result.status === 0) {
329
- const currentCommand = result.stdout.trim().toLowerCase();
330
- if (currentCommand.includes("claude")) return true;
331
- }
32
+ // Sending signal 0 checks if process exists without actually sending a signal
33
+ process.kill(pid, 0);
34
+ return true;
332
35
  } catch {
333
- // Ignore
36
+ return false;
334
37
  }
335
-
336
- return false;
337
38
  }
338
39
 
339
40
  /**
@@ -355,6 +56,7 @@ function checkSessionLockFile(projectPath) {
355
56
  const isAlive = processExists(lockData.pid);
356
57
  if (!isAlive) {
357
58
  // Stale lock file, clean it up
59
+ log.debug({ lockFile, pid: lockData.pid }, "Removing stale lock");
358
60
  try {
359
61
  fs.unlinkSync(lockFile);
360
62
  } catch {
@@ -377,83 +79,43 @@ function checkSessionLockFile(projectPath) {
377
79
  return { exists: false, lockFile: null, content: null };
378
80
  }
379
81
 
380
- /**
381
- * Check if a process exists
382
- * @param {number} pid - Process ID
383
- * @returns {boolean}
384
- */
385
- function processExists(pid) {
386
- try {
387
- // Sending signal 0 checks if process exists without actually sending a signal
388
- process.kill(pid, 0);
389
- return true;
390
- } catch {
391
- return false;
392
- }
393
- }
394
-
395
82
  /**
396
83
  * Main detection function - detect all external Claude sessions
84
+ * Uses cached process data for instant response.
85
+ *
397
86
  * @param {string} projectPath - The project directory to check
398
- * @returns {{ hasExternalSession: boolean, processes: Array, tmuxSessions: Array, lockFile: object, detectionAvailable: boolean, detectionError: string | null }}
87
+ * @returns {{ hasExternalSession: boolean, processes: Array, tmuxSessions: Array, lockFile: object, detectionAvailable: boolean, detectionError: string | null, cacheAge: number | null }}
399
88
  */
400
89
  function detectExternalClaude(projectPath) {
401
- console.log("[detectExternalClaude] Called with projectPath:", projectPath);
90
+ log.debug({ projectPath }, "Detection requested");
402
91
 
403
- // Check cache
404
- const cacheKey = projectPath || "__global__";
405
- const cached = detectionCache.get(cacheKey);
406
- if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
407
- console.log("[detectExternalClaude] Returning cached result");
408
- return cached.result;
409
- }
410
- console.log("[detectExternalClaude] Cache miss, performing fresh detection");
92
+ // Get cached process data (instant - no process scanning)
93
+ const cachedData = getCachedProcessData();
94
+ const cacheAge = getCacheAge();
411
95
 
412
96
  const result = {
413
97
  hasExternalSession: false,
414
- processes: [],
415
- tmuxSessions: [],
98
+ processes: [...cachedData.processes],
99
+ tmuxSessions: [...cachedData.tmuxSessions],
416
100
  lockFile: { exists: false, lockFile: null, content: null },
417
- detectionAvailable: true,
418
- detectionError: null,
101
+ detectionAvailable: cachedData.detectionAvailable,
102
+ detectionError: cachedData.detectionError,
103
+ cacheAge,
419
104
  };
420
105
 
421
- // Detect processes
422
- const processDetection = detectClaudeProcesses();
423
- result.processes = processDetection.processes;
424
- result.detectionAvailable = processDetection.detectionAvailable;
425
- result.detectionError = processDetection.error;
426
- console.log(
427
- "[detectExternalClaude] Process detection result:",
428
- processDetection.processes.length,
429
- "processes, available:",
430
- processDetection.detectionAvailable,
431
- "error:",
432
- processDetection.error,
433
- );
434
-
435
- if (projectPath) {
436
- // Filter to processes in this project
106
+ // Filter processes to project if path provided
107
+ if (projectPath && result.processes.length > 0) {
437
108
  const beforeFilter = result.processes.length;
438
109
  result.processes = result.processes.filter(
439
110
  (p) => !p.cwd || p.cwd.startsWith(projectPath),
440
111
  );
441
- console.log(
442
- "[detectExternalClaude] Filtered processes for project:",
443
- beforeFilter,
444
- "->",
445
- result.processes.length,
112
+ log.debug(
113
+ { projectPath, before: beforeFilter, after: result.processes.length },
114
+ "Filtered processes for project",
446
115
  );
447
116
  }
448
117
 
449
- // Detect tmux sessions
450
- result.tmuxSessions = detectClaudeTmuxSessions();
451
- console.log(
452
- "[detectExternalClaude] tmux sessions found:",
453
- result.tmuxSessions.length,
454
- );
455
-
456
- // Check lock file
118
+ // Check lock file (fast filesystem operation)
457
119
  if (projectPath) {
458
120
  result.lockFile = checkSessionLockFile(projectPath);
459
121
  }
@@ -464,22 +126,20 @@ function detectExternalClaude(projectPath) {
464
126
  result.tmuxSessions.length > 0 ||
465
127
  result.lockFile.exists;
466
128
 
467
- // Cache the result
468
- detectionCache.set(cacheKey, {
469
- timestamp: Date.now(),
470
- result,
471
- });
129
+ log.debug(
130
+ {
131
+ hasExternalSession: result.hasExternalSession,
132
+ processCount: result.processes.length,
133
+ tmuxCount: result.tmuxSessions.length,
134
+ hasLockFile: result.lockFile.exists,
135
+ cacheAge,
136
+ },
137
+ "Detection complete",
138
+ );
472
139
 
473
140
  return result;
474
141
  }
475
142
 
476
- /**
477
- * Clear the detection cache
478
- */
479
- function clearCache() {
480
- detectionCache.clear();
481
- }
482
-
483
143
  /**
484
144
  * Create a session lock file for this application
485
145
  * @param {string} projectPath - The project directory
@@ -504,12 +164,10 @@ function createSessionLock(projectPath, sessionId) {
504
164
  };
505
165
 
506
166
  fs.writeFileSync(lockFile, JSON.stringify(lockData, null, 2));
167
+ log.debug({ lockFile, sessionId }, "Created session lock");
507
168
  return true;
508
169
  } catch (err) {
509
- console.error(
510
- "[ExternalSessionDetector] Failed to create lock file:",
511
- err.message,
512
- );
170
+ log.error({ error: err.message, lockFile }, "Failed to create lock file");
513
171
  return false;
514
172
  }
515
173
  }
@@ -525,6 +183,7 @@ function removeSessionLock(projectPath) {
525
183
  try {
526
184
  if (fs.existsSync(lockFile)) {
527
185
  fs.unlinkSync(lockFile);
186
+ log.debug({ lockFile }, "Removed session lock");
528
187
  }
529
188
  return true;
530
189
  } catch {
@@ -534,10 +193,7 @@ function removeSessionLock(projectPath) {
534
193
 
535
194
  export {
536
195
  detectExternalClaude,
537
- detectClaudeProcesses,
538
- detectClaudeTmuxSessions,
539
196
  checkSessionLockFile,
540
197
  createSessionLock,
541
198
  removeSessionLock,
542
- clearCache,
543
199
  };