@epiphytic/claudecodeui 1.1.0 → 1.2.1

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.
Files changed (55) hide show
  1. package/dist/assets/index-BGneYLVE.css +32 -0
  2. package/dist/assets/{index-D0xTNXrF.js → index-sqmQ9jF8.js} +210 -212
  3. package/dist/index.html +2 -2
  4. package/dist/sw.js +25 -1
  5. package/package.json +2 -1
  6. package/public/api-docs.html +879 -0
  7. package/public/clear-cache.html +85 -0
  8. package/public/convert-icons.md +53 -0
  9. package/public/favicon.png +0 -0
  10. package/public/favicon.svg +9 -0
  11. package/public/generate-icons.js +49 -0
  12. package/public/icons/claude-ai-icon.svg +1 -0
  13. package/public/icons/codex-white.svg +3 -0
  14. package/public/icons/codex.svg +3 -0
  15. package/public/icons/cursor-white.svg +12 -0
  16. package/public/icons/cursor.svg +1 -0
  17. package/public/icons/generate-icons.md +19 -0
  18. package/public/icons/icon-128x128.png +0 -0
  19. package/public/icons/icon-128x128.svg +12 -0
  20. package/public/icons/icon-144x144.png +0 -0
  21. package/public/icons/icon-144x144.svg +12 -0
  22. package/public/icons/icon-152x152.png +0 -0
  23. package/public/icons/icon-152x152.svg +12 -0
  24. package/public/icons/icon-192x192.png +0 -0
  25. package/public/icons/icon-192x192.svg +12 -0
  26. package/public/icons/icon-384x384.png +0 -0
  27. package/public/icons/icon-384x384.svg +12 -0
  28. package/public/icons/icon-512x512.png +0 -0
  29. package/public/icons/icon-512x512.svg +12 -0
  30. package/public/icons/icon-72x72.png +0 -0
  31. package/public/icons/icon-72x72.svg +12 -0
  32. package/public/icons/icon-96x96.png +0 -0
  33. package/public/icons/icon-96x96.svg +12 -0
  34. package/public/icons/icon-template.svg +12 -0
  35. package/public/logo-128.png +0 -0
  36. package/public/logo-256.png +0 -0
  37. package/public/logo-32.png +0 -0
  38. package/public/logo-512.png +0 -0
  39. package/public/logo-64.png +0 -0
  40. package/public/logo.svg +17 -0
  41. package/public/manifest.json +61 -0
  42. package/public/screenshots/cli-selection.png +0 -0
  43. package/public/screenshots/desktop-main.png +0 -0
  44. package/public/screenshots/mobile-chat.png +0 -0
  45. package/public/screenshots/tools-modal.png +0 -0
  46. package/public/sw.js +131 -0
  47. package/server/database/db.js +98 -0
  48. package/server/database/init.sql +13 -1
  49. package/server/external-session-detector.js +188 -48
  50. package/server/index.js +210 -7
  51. package/server/orchestrator/client.js +361 -16
  52. package/server/orchestrator/index.js +83 -8
  53. package/server/orchestrator/protocol.js +67 -0
  54. package/server/projects.js +2 -1
  55. package/dist/assets/index-DKDK7xNY.css +0 -32
@@ -120,6 +120,27 @@ const runMigrations = () => {
120
120
  `);
121
121
  }
122
122
 
123
+ // Check if orchestrator_tokens table exists
124
+ const orchestratorTokensTable = db
125
+ .prepare(
126
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='orchestrator_tokens'",
127
+ )
128
+ .all();
129
+ if (orchestratorTokensTable.length === 0) {
130
+ console.log("Running migration: Creating orchestrator_tokens table");
131
+ db.exec(`
132
+ CREATE TABLE IF NOT EXISTS orchestrator_tokens (
133
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
134
+ host TEXT NOT NULL UNIQUE,
135
+ token TEXT NOT NULL,
136
+ client_id TEXT,
137
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
138
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
139
+ );
140
+ CREATE INDEX IF NOT EXISTS idx_orchestrator_tokens_host ON orchestrator_tokens(host);
141
+ `);
142
+ }
143
+
123
144
  console.log("Database migrations completed successfully");
124
145
  } catch (error) {
125
146
  console.error("Error running migrations:", error.message);
@@ -642,6 +663,82 @@ const tmuxSessionsDb = {
642
663
  },
643
664
  };
644
665
 
666
+ // Orchestrator tokens database operations (for storing tokens received during pending mode)
667
+ const orchestratorTokensDb = {
668
+ /**
669
+ * Get stored orchestrator token for a specific host
670
+ * @param {string} host - The orchestrator host (e.g., "duratii.example.com")
671
+ * @returns {{token: string, client_id: string} | null}
672
+ */
673
+ getToken: (host) => {
674
+ try {
675
+ const row = db
676
+ .prepare(
677
+ "SELECT token, client_id FROM orchestrator_tokens WHERE host = ?",
678
+ )
679
+ .get(host);
680
+ return row || null;
681
+ } catch (err) {
682
+ throw err;
683
+ }
684
+ },
685
+
686
+ /**
687
+ * Save or update orchestrator token for a host
688
+ * @param {string} host - The orchestrator host
689
+ * @param {string} token - The full token string
690
+ * @param {string} clientId - The client ID from orchestrator
691
+ */
692
+ saveToken: (host, token, clientId) => {
693
+ try {
694
+ const stmt = db.prepare(`
695
+ INSERT INTO orchestrator_tokens (host, token, client_id, updated_at)
696
+ VALUES (?, ?, ?, CURRENT_TIMESTAMP)
697
+ ON CONFLICT(host) DO UPDATE SET
698
+ token = excluded.token,
699
+ client_id = excluded.client_id,
700
+ updated_at = CURRENT_TIMESTAMP
701
+ `);
702
+ stmt.run(host, token, clientId);
703
+ return true;
704
+ } catch (err) {
705
+ throw err;
706
+ }
707
+ },
708
+
709
+ /**
710
+ * Delete orchestrator token for a host
711
+ * @param {string} host - The orchestrator host
712
+ * @returns {boolean} True if a token was deleted
713
+ */
714
+ deleteToken: (host) => {
715
+ try {
716
+ const stmt = db.prepare("DELETE FROM orchestrator_tokens WHERE host = ?");
717
+ const result = stmt.run(host);
718
+ return result.changes > 0;
719
+ } catch (err) {
720
+ throw err;
721
+ }
722
+ },
723
+
724
+ /**
725
+ * Get all stored orchestrator tokens
726
+ * @returns {Array<{id: number, host: string, client_id: string, created_at: string, updated_at: string}>}
727
+ */
728
+ getAllTokens: () => {
729
+ try {
730
+ const rows = db
731
+ .prepare(
732
+ "SELECT id, host, client_id, created_at, updated_at FROM orchestrator_tokens ORDER BY updated_at DESC",
733
+ )
734
+ .all();
735
+ return rows;
736
+ } catch (err) {
737
+ throw err;
738
+ }
739
+ },
740
+ };
741
+
645
742
  // Backward compatibility - keep old names pointing to new system
646
743
  const githubTokensDb = {
647
744
  createGithubToken: (userId, tokenName, githubToken, description = null) => {
@@ -675,4 +772,5 @@ export {
675
772
  credentialsDb,
676
773
  githubTokensDb, // Backward compatibility
677
774
  tmuxSessionsDb,
775
+ orchestratorTokensDb,
678
776
  };
@@ -65,4 +65,16 @@ CREATE TABLE IF NOT EXISTS tmux_sessions (
65
65
  );
66
66
 
67
67
  CREATE INDEX IF NOT EXISTS idx_tmux_sessions_project ON tmux_sessions(project_path);
68
- CREATE INDEX IF NOT EXISTS idx_tmux_sessions_name ON tmux_sessions(tmux_session_name);
68
+ CREATE INDEX IF NOT EXISTS idx_tmux_sessions_name ON tmux_sessions(tmux_session_name);
69
+
70
+ -- Orchestrator tokens table for storing tokens received from orchestrator during pending mode
71
+ CREATE TABLE IF NOT EXISTS orchestrator_tokens (
72
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
73
+ host TEXT NOT NULL UNIQUE, -- e.g., "duratii.example.com"
74
+ token TEXT NOT NULL, -- Full token string (ao_xxx_yyy)
75
+ client_id TEXT, -- Client ID assigned by orchestrator
76
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
77
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
78
+ );
79
+
80
+ CREATE INDEX IF NOT EXISTS idx_orchestrator_tokens_host ON orchestrator_tokens(host);
@@ -27,10 +27,16 @@ const currentPid = process.pid;
27
27
 
28
28
  /**
29
29
  * Detect external Claude processes
30
- * @returns {Array<{ pid: number, command: string, cwd: string | null }>}
30
+ * @returns {{ processes: Array<{ pid: number, command: string, cwd: string | null }>, detectionAvailable: boolean, error: string | null }}
31
31
  */
32
32
  function detectClaudeProcesses() {
33
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);
34
40
 
35
41
  if (os.platform() === "win32") {
36
42
  // Windows: use wmic or tasklist
@@ -58,72 +64,165 @@ function detectClaudeProcesses() {
58
64
  }
59
65
  }
60
66
  }
67
+ } else if (result.error) {
68
+ detectionAvailable = false;
69
+ error = `wmic not available: ${result.error.message}`;
61
70
  }
62
- } catch {
63
- // Ignore errors on Windows
71
+ } catch (e) {
72
+ detectionAvailable = false;
73
+ error = `Windows process detection failed: ${e.message}`;
64
74
  }
65
75
  } else {
66
76
  // Unix: use pgrep and ps
67
77
  try {
68
- // First, get PIDs of claude processes
69
- const pgrepResult = spawnSync("pgrep", ["-f", "claude"], {
78
+ // First, check if pgrep is available
79
+ const pgrepCheck = spawnSync("which", ["pgrep"], {
70
80
  encoding: "utf8",
71
81
  stdio: "pipe",
72
82
  });
83
+ console.log(
84
+ "[ExternalSessionDetector] pgrep available:",
85
+ pgrepCheck.status === 0,
86
+ );
73
87
 
74
- if (pgrepResult.status === 0) {
75
- const pids = pgrepResult.stdout.trim().split("\n").filter(Boolean);
76
-
77
- for (const pidStr of pids) {
78
- const pid = parseInt(pidStr, 10);
79
-
80
- // Skip our own process and child processes
81
- if (pid === currentPid) continue;
82
-
83
- // Get command details
84
- const psResult = spawnSync("ps", ["-p", String(pid), "-o", "args="], {
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"], {
85
93
  encoding: "utf8",
86
94
  stdio: "pipe",
87
95
  });
88
96
 
89
97
  if (psResult.status === 0) {
90
- const command = psResult.stdout.trim();
91
-
92
- // Filter out our own subprocesses (claude-sdk spawned by this app)
93
- // and only include standalone claude CLI invocations
94
- if (isExternalClaudeProcess(command)) {
95
- // Try to get working directory via lsof
96
- let cwd = null;
97
- try {
98
- const lsofResult = spawnSync(
99
- "lsof",
100
- ["-p", String(pid), "-Fn"],
101
- {
102
- encoding: "utf8",
103
- stdio: "pipe",
104
- },
105
- );
106
- if (lsofResult.status === 0) {
107
- const cwdMatch = lsofResult.stdout.match(/n(\/[^\n]+)/);
108
- if (cwdMatch) {
109
- cwd = cwdMatch[1];
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 });
110
122
  }
111
123
  }
112
- } catch {
113
- // lsof may not be available
114
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
+ }
115
202
 
116
- processes.push({ pid, command, cwd });
203
+ processes.push({ pid, command, cwd });
204
+ }
117
205
  }
118
206
  }
207
+ } else {
208
+ console.log(
209
+ "[ExternalSessionDetector] pgrep found no claude processes (status:",
210
+ pgrepResult.status,
211
+ ")",
212
+ );
119
213
  }
120
214
  }
121
- } catch {
122
- // Ignore errors
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
+ );
123
222
  }
124
223
  }
125
224
 
126
- return processes;
225
+ return { processes, detectionAvailable, error };
127
226
  }
128
227
 
129
228
  /**
@@ -133,18 +232,30 @@ function detectClaudeProcesses() {
133
232
  */
134
233
  function isExternalClaudeProcess(command) {
135
234
  // Skip node processes (SDK internals)
136
- if (command.startsWith("node ")) return false;
235
+ if (command.startsWith("node ")) {
236
+ console.log("[isExternalClaudeProcess] Rejected: starts with 'node '");
237
+ return false;
238
+ }
137
239
 
138
240
  // Skip our own server
139
- if (command.includes("claudecodeui/server")) return false;
241
+ if (command.includes("claudecodeui/server")) {
242
+ console.log(
243
+ "[isExternalClaudeProcess] Rejected: contains 'claudecodeui/server'",
244
+ );
245
+ return false;
246
+ }
140
247
 
141
248
  // Look for actual claude CLI invocations
142
- return (
249
+ const isExternal =
143
250
  command.includes("claude ") ||
144
251
  command.includes("claude-code") ||
145
252
  command.match(/\/claude\s/) ||
146
- command.endsWith("/claude")
253
+ command.endsWith("/claude");
254
+
255
+ console.log(
256
+ `[isExternalClaudeProcess] "${command.slice(0, 60)}..." => ${isExternal}`,
147
257
  );
258
+ return isExternal;
148
259
  }
149
260
 
150
261
  /**
@@ -284,34 +395,63 @@ function processExists(pid) {
284
395
  /**
285
396
  * Main detection function - detect all external Claude sessions
286
397
  * @param {string} projectPath - The project directory to check
287
- * @returns {{ hasExternalSession: boolean, processes: Array, tmuxSessions: Array, lockFile: object }}
398
+ * @returns {{ hasExternalSession: boolean, processes: Array, tmuxSessions: Array, lockFile: object, detectionAvailable: boolean, detectionError: string | null }}
288
399
  */
289
400
  function detectExternalClaude(projectPath) {
401
+ console.log("[detectExternalClaude] Called with projectPath:", projectPath);
402
+
290
403
  // Check cache
291
404
  const cacheKey = projectPath || "__global__";
292
405
  const cached = detectionCache.get(cacheKey);
293
406
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
407
+ console.log("[detectExternalClaude] Returning cached result");
294
408
  return cached.result;
295
409
  }
410
+ console.log("[detectExternalClaude] Cache miss, performing fresh detection");
296
411
 
297
412
  const result = {
298
413
  hasExternalSession: false,
299
414
  processes: [],
300
415
  tmuxSessions: [],
301
416
  lockFile: { exists: false, lockFile: null, content: null },
417
+ detectionAvailable: true,
418
+ detectionError: null,
302
419
  };
303
420
 
304
421
  // Detect processes
305
- result.processes = detectClaudeProcesses();
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
+
306
435
  if (projectPath) {
307
436
  // Filter to processes in this project
437
+ const beforeFilter = result.processes.length;
308
438
  result.processes = result.processes.filter(
309
439
  (p) => !p.cwd || p.cwd.startsWith(projectPath),
310
440
  );
441
+ console.log(
442
+ "[detectExternalClaude] Filtered processes for project:",
443
+ beforeFilter,
444
+ "->",
445
+ result.processes.length,
446
+ );
311
447
  }
312
448
 
313
449
  // Detect tmux sessions
314
450
  result.tmuxSessions = detectClaudeTmuxSessions();
451
+ console.log(
452
+ "[detectExternalClaude] tmux sessions found:",
453
+ result.tmuxSessions.length,
454
+ );
315
455
 
316
456
  // Check lock file
317
457
  if (projectPath) {